Store options
createStore(name, useFn, options?) takes an options object as its third argument.
createStore('name', useFn, {
timeToClean: 5000, // keep the instance alive 5 s after its last reader leaves (default 0)
schedule: frame(), // when readers re-render for a change (default: at once)
})Stores are lazy: nothing exists until the first reader asks, and until the hook has run once its values read as undefined. Readers default at the read with ?? or ?.; see Before the data arrives.
timeToClean
How long an instance stays mounted after its last consumer unmounts, in milliseconds. Default 0: the instance is torn down right away. A positive value keeps state and effects alive across quick unmount/remount sequences such as route changes or tab switches.
createStore('search', useSearchState, { timeToClean: 30_000 })Retainers from storeRef(params).retain() count as readers.
Infinity keeps the instance until AutoRootCtx unmounts: for a store the whole app shares, such as the session or a socket connection. Any value of 2³¹ − 1 ms (about 24.8 days) or more does the same. Earlier versions tore such an instance down at once, because a timer that long fires immediately.
The instance keeps running during that time, effects included: a store that polls keeps polling, and a socket stays open. That is what you want when the next screen needs the live resource right away.
Keeping the values, not the resource
To keep only the values once the screen closes, split the store: one store holds the values and has no effects, kept with a long timeToClean; another runs the resource with the default timeToClean: 0 and writes into the first. Coming back shows the kept values at once while the resource reconnects.
// the values: no effects, kept for five minutes after the last reader leaves
export const { useStore: useRoomHistory } = createStore('room-history', ({ roomId }: { roomId: string }) => {
const [messages, setMessages] = useState<Message[]>([])
return { messages, setMessages }
}, { timeToClean: 5 * 60_000 })
// the resource: the socket closes as soon as the last screen leaves
export const { useStore: useRoom } = createStore('room', ({ roomId }: { roomId: string }) => {
const { messages, setMessages } = useRoomHistory({ roomId })
useEffect(() => {
if (!setMessages) return
return socket.subscribe(roomId, message => setMessages(list => [...list, message]))
}, [roomId, setMessages])
return { messages }
})The Live Rooms example of the demo runs this split. Server data fetched through a query library can stay in that library's cache instead: a store mounted again starts from the cached response while it refetches.
schedule
When the store's readers re-render for a change: a scheduler such as frame(), throttle(ms), debounce(ms, { maxWait }) or idle(ms), imported from react-state-custom/schedulers (default: at once). A reader can pass its own: useStore(params, { schedule }). The store itself, storeRef(params).get() and actions are never delayed. See Update cadence.
// equity moves with every price tick: its readers show it four times a second
createStore('portfolio', usePortfolioState, { schedule: throttle(250) })Side effects once per instance
A store hook runs once per instance, so an effect in it runs once per instance too, however many components read the store:
const useUserState = ({ userId }: { userId: string }) => {
useEffect(() => { track('user-store-mounted', userId) }, [userId])
// ...
}