Store
A simple store for complex state within a widget, similar to zustand. Components subscribe to a slice of the store's state and only re-render when this slice changes.
This is especially helpful with repeater fields, when the items need to share information with each other or with the rest of the widget.
function createStore<T>(initialState: T): Store<T>
function useStoreSelector<TReturnValue, TStore>(
context: Context<Store<TStore>>,
selector: (state: TStore) => TReturnValue,
equals?: (a: TReturnValue, b: TReturnValue) => boolean
): TReturnValue
createStore
Creates a store with the given initial state. The returned store has the following methods:
| Method | Type | Description |
|---|---|---|
getState | () => T | Returns the current state. |
setState | (action: T | ((prev: T) => T)) => void | Replaces the state, either with the given state or with the state returned by the function. Notifies all subscribers. |
subscribe | (callback: () => void) => () => void | Calls the callback on every state change. Returns a function to unsubscribe. |
useStoreSelector
Selects a slice of the state of the store provided by the given React context, and re-renders the component when the slice changes.
| Argument | Type | Description |
|---|---|---|
| context | Context<Store<TStore>> | The React context providing the store. |
| selector | (state: TStore) => TReturnValue | Selects the slice from the state. |
| equals | (a: TReturnValue, b: TReturnValue) => boolean | Compares slices. Defaults to a deep comparison using isEqual of lodash. |
Example
Each item of a repeater reports its title to the store, and the widget displays the number of titles:
import React, {createContext, PropsWithChildren, useCallback, useContext, useEffect, useState} from 'react'
import {createStore, Repeater, useStoreSelector, useStringField} from '@modbros/dashboard-sdk'
interface TitleState {
titles: string[]
}
const TitleStoreContext = createContext(createStore<TitleState>({titles: []}))
// Creates a separate store for every instance of the widget
const TitleStoreProvider = (props: PropsWithChildren) => {
const {children} = props
const [store] = useState(() => createStore<TitleState>({titles: []}))
return <TitleStoreContext.Provider value={store}>{children}</TitleStoreContext.Provider>
}
function useSetTitle() {
const store = useContext(TitleStoreContext)
return useCallback(
(index: number, title: string) => {
store.setState((prev) => {
const titles = [...prev.titles]
titles[index] = title
return {...prev, titles}
})
},
[store]
)
}
function useTitleCount() {
return useStoreSelector(TitleStoreContext, (state) => state.titles.filter(Boolean).length)
}
const Item = (props: {index: number}) => {
const {index} = props
const title = useStringField({field: 'title'})
const setTitle = useSetTitle()
useEffect(() => {
setTitle(index, title ?? '')
}, [index, title, setTitle])
return <li>{title}</li>
}
const Summary = () => {
const count = useTitleCount()
return <strong>{count} titles</strong>
}
export default function Example() {
return (
<TitleStoreProvider>
<Summary/>
<ul>
<Repeater field="items">{(item, index) => <Item index={index}/>}</Repeater>
</ul>
</TitleStoreProvider>
)
}
The store passed to createContext is created once when the module is loaded, and is therefore shared by all
instances of the widget on a dashboard. Always provide a store per widget instance, like TitleStoreProvider in the
example above.