Concise 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 { Store, useStore } from "@t8/react-store";
+
+ let counterStore = new Store(0);
let Counter = () => {
- let [counter, setCounter] = useState(0);
+ let [counter, setCounter] = useStore(counterStore);
let handleClick = () => {
setCounter(value => value + 1);
};
return <button onClick={handleClick}>+ {counter}</button>;
};
let ResetButton = () => {
- let [, setCounter] = useState(0);
+ let [, setCounter] = useStore(counterStore, false);
let handleClick = () => {
setCounter(0);
};
return <button onClick={handleClick}>×</button>;
};
let App = () => <><Counter/>{" "}<ResetButton/></>;
🔹 The shared state setup shown above 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.
🔹 Splitting data into multiple stores is one of the strategies to maintain more targeted subscriptions to data changes in components. The other strategy is filtering store updates at the component level, which is discussed below.
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)
accepts 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(itemStore, hasRelevantUpdates);
return (
// Content
);
};
Shared state can be provided to the app by means of a regular React Context provider:
import { createContext } from "react";
export let AppContext = createContext(new Store(0));
let App = () => (
<AppContext.Provider value={new Store(42)}>
<PlusButton/>{" "}<Display/>
</AppContext.Provider>
);
let Counter = () => {
let [counter, setCounter] = useStore(useContext(AppContext));
// Rendering
};
Live counter demo with Context
🔹 In a multi-store setup, stores can be located in a single Context or split across multiple Contexts, just like any application data.
// Multiple stores in a single Context
let AppContext = createContext({
users: new Store(/* ... */),
items: new Store(/* ... */),
});
let ItemCard = ({ id }) => {
let [items, setItems] = useStore(useContext(AppContext).items);
// Rendering
};
🔹 Note that updating the store state doesn't change the store reference sitting in the React Context and therefore doesn't cause updates of the entire Context. Only the components subscribed to updates in the particular store by means of useStore(store)
will be notified to re-render.
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 standalone store initialized outside a component can be used by the component as remount-persistent state, whether used by other components or not.