Straightforward and minimalist shared state management for React apps
- Similar to
useState()
- No boilerplate
- Painless transition from local state to shared state and vice versa
- SSR-compatible
Installation: npm i @t8/react-store
Moving the local state to the full-fledged shared state:
import { createContext, useContext } from "react";
+ import { Store, useStore } from "@t8/react-store";
+
+ let AppContext = createContext(new Store(0));
let Counter = () => {
- let [counter, setCounter] = useState(0);
+ let [counter, setCounter] = useStore(useContext(AppContext));
let handleClick = () => {
setCounter(value => value + 1);
};
return <button onClick={handleClick}>{counter}</button>;
};
let ResetButton = () => {
- let [, setCounter] = useState(0);
+ let [, setCounter] = useStore(useContext(AppContext), false);
let handleClick = () => {
setCounter(0);
};
return <button onClick={handleClick}>×</button>;
};
let App = () => <><Counter/>{" "}<ResetButton/></>;
🔹 The shared state setup with @t8/react-store
is very similar to useState()
allowing for quick migration from local state to shared state or the other way around.
🔹 The false
parameter in useStore(store, false)
(as in <ResetButton>
above) tells the hook not to subscribe the component to tracking the store state updates. The common use case is when a component makes use of the store state setter without using the store state value.
An application can have as many stores as needed, whether on a single React Context or multiple Contexts.
let AppContext = createContext({
users: new Store(/* ... */),
items: new Store(/* ... */),
});
🔹 Splitting data into multiple stores helps maintain more targeted subscriptions to data changes in components.
When only the store state setter is required, without the store state value, we can opt out from subscription to store state changes by passing false
as the parameter of useStore()
:
let [, setState] = useState(store, false);
Apart from a boolean, useStore(store, shouldUpdate)
can take a function of (nextState, prevState) => boolean
as the second parameter to filter store updates to respond to:
let ItemCard = ({ id }) => {
// Definition of changes in the item
let hasRelevantUpdates = useCallback((nextItems, prevItems) => {
// Assuming that items have a `revision` property
return nextItems[id].revision !== prevItems[id].revision;
}, [id]);
let [items, setItems] = useStore(
useContext(AppContext).items,
hasRelevantUpdates,
);
return (
// Content
);
};
Shared state can be provided to the app by means of a regular React Context provider:
let App = () => (
- <AppContext.Provider value={42}>
+ <AppContext.Provider value={new Store(42)}>
<PlusButton/>{" "}<Display/>
</AppContext.Provider>
);
A store can contain data of any type.
Live demos:
Primitive value state
Object value state
Immer can be used with useStore()
just the same way as with useState()
to facilitate deeply nested data changes.
The ready-to-use hook from the T8 React Pending package helps manage shared async action state without disturbing the app's state management and actions' code.
A store initialized outside a component can be used as the component's remount-persistent state.