Underrated React API - useSyncExternalStore and Understanding Zustand
Global External Store Subscription Framework
Intention - Either React's effort in useSyncExternalStore or the recent innovation in state management libraries; the underlying idea was the same. How to avoid unessential re-renders (caused while managing the state)?
useSyncExternalStore:
This api lets components subscribe to an external store(without any wrapper). The external store is the state that React is not aware of. React can only handle re-renders of the state they know of. An intuitive thought would be to access external store via React's state like useState, useReducer, or reducer/context combination. Such approaches are not scalable, not easy to maintain, and do not support subscribing to a specific subset of the state from context.
Now onwards I'll refer useSyncExternalStore as uSES for a simplified explanation.
Example: use-count.js
const subscribers = new Set();
let count = 0;
export const countState = {
subscribe: function (cb) {
subscribers.add(cb);
return () => subscribers.delete(cb);
},
incrementCount: function () {
count = count + 1;
console.log("count", count);
subscribers.forEach((cb) => cb());
},
getCount: function () {
return count;
},
};
The above snippet is of state-maintainer which will be passed to uSES, it consists of the following:
state snapshot - count in this case
setter functions to modify state - incrementCount
subscribe function - will be passed to uSES to capture the interested components, and we'll store them in variable subscribers.
get snapshot - getCount function that returns state, again will be passed to uSES.
sibling2.jsx
const counter = useSyncExternalStore(
countState.subscribe,
countState.getCount
);
return (
<div className="sibling2">
Sibling2:
<button onClick={() => countState.incrementCount()}>
count is {counter}
</button>
</div>
);
The previously created state maintainer can be used in component files for integration with uSES.
Zustand:
Zustand utilizes uSES and sprinkles some extra benefits to make managing the global state in applications easier. Provides the above-mentioned selection-based subscription by memoizing snapshots according to the given selection.
Global External Store Subscription Framework
The idea is to extract maintainer logic to form generic code, that will support all sorts of states exclusively maintaining respective subscribers, handling of event actions, and providing state.
Create maintain.js
export function maintain(createState) {
let state;
const subscribers = new Set();
const setter = (setCallback) => {
const changedPartialState = setCallback(state);
state = Object.assign({}, state, changedPartialState);
subscribers.forEach((reRender) => reRender());
};
state = createState(setter);
const subscribe = (subscribeCallback) => {
subscribers.add(subscribeCallback);
return () => subscribers.delete(subscribeCallback);
};
const getSnapshot = () => state;
return {
subscribe,
getSnapshot,
};
}
Here maintain.js
has parameter createState function, which is called to get the initial state. Function createState is called with argument setter callback, this callback is used to access the results of event actions (like incrementCount) so that their results can be in sync with our state instance. Additionally, after each update setter makes sure to call render callback so that the subscribers can get the latest instance of the state.
create.js
export function create(createState) {
const { subscribe, getSnapshot } = maintain(createState);
return (selection) =>
useSyncExternalStoreWithSelector(
subscribe,
getSnapshot,
undefined,
selection
);
}
create.js
is a connecting bridge between maintainer logic and the state (createState). uSESWithSelector is uSES shim provided by React.
uSES does not support complex mutable state like objects because of React's nature of comparison Object.is, this shim provides a selection based subscription. It memoizes a subset of snapshot i.e. selection and the snapshot. So, if states have nested objects, selectors should lead to primitive values otherwise Object.is will return false in every re-render calls and will keep re-rendering repeatedly.
use-fruit.js
export const useFruit = useAnywhere((set) => ({
apple: 0,
orange: 0,
incrementApple: () => set((state) => ({ apple: state.apple + 1 })),
incrementOrange: () => set((state) => ({ orange: state.orange + 1 })),
}));
useFruit utilizes our generic framework focusing just on the state and its actions, and not on maintenance or render logic, you see it reduces the repetition of subscribe, getState and uSES code for every state.
Complete code:
Here, the component has access to the global fruit state but is only subscribed to state.apple
because of its selection callback
Memoization in the shim is optimization but selection callback should return a primitive value because at the end that is being passed to uSES internally in the shim. (There is commented code in create.js which uses just uSES and not the shim)
Epilogue: State sharing without wrapper and subset state subscription may seem like magic but its working of uSES has made this possible.