Evolution of React State Management: From Local Hooks to Unstated‑Next, Hox, and Zustand
The article traces React’s state‑management evolution from basic useState hooks through unstated‑next and hox to the minimalist zustand library, showing how each step reduces boilerplate, eliminates prop‑drilling and deep providers, and enables precise, hook‑based updates for modern functional components.
Since React 16.8, function components have become mainstream, and the landscape of state‑management solutions has dramatically changed. While Redux remains a dominant approach, it suffers from excessive concepts, high learning cost, boilerplate code, and the need for middleware.
Hooks introduced a more elegant and concise way to manage state, prompting the community to create lightweight libraries such as unstated‑next , hox , and zustand . A classic example used to illustrate any state‑management library is a simple counter that increments with "+" and decrements with "-".
React local state hooks
React’s built‑in hooks API ( useState ) can implement a counter with just a few lines. The state and its updater functions are defined in the root component and passed down through props.
// timer.js
const Timer = (props) => {
const { increment, count, decrement } = props;
return (
<>
-
{count}
+
);
};
// app.js
const App = () => {
const [count, setCount] = React.useState(0);
const increment = () => setCount(count + 1);
const decrement = () => setCount(count - 1);
return
;
};However, this approach tightly couples business logic with the component, makes the root component a “large monster”, and requires prop‑drilling for shared state.
unstated‑next
To address coupling, a custom hook (e.g., useCount ) can encapsulate the counter logic, and React Context can share the state without prop‑drilling.
function useCount() {
const [count, setCount] = React.useState(0);
const increment = () => setCount(count + 1);
const decrement = () => setCount(count - 1);
return { count, increment, decrement };
}Creating a container abstracts the Context definition and usage:
function createContainer(useHook) {
const StoreContext = React.createContext();
function useContainer() {
const store = React.useContext(StoreContext);
return store;
}
function Provider(props) {
const store = useHook();
return
{props.children}
;
}
return { Provider, useContainer };
}
// Usage
const Store = createContainer(useCount);
// timer.js
const Timer = () => {
const store = Store.useContainer();
// render omitted
};
// app.js
const App = () => (
);Although cleaner, this still requires defining many Contexts and importing them in each component.
hox
hox refines the pattern by eliminating explicit Provider nesting and enabling precise updates. The core model consists of a Store, a Hook (business logic), and Components that consume the Store.
Creating a container returns a hook that subscribes to a Set of listeners:
function createContainer(hook) {
const store = hook();
const listeners = new Set();
function useContainer() {
const storeRef = useRef(store);
useEffect(() => {
listeners.add(listener);
return () => listeners.delete(listener);
}, []);
return storeRef.current;
}
return useContainer;
}Components force a re‑render via useReducer when the Store changes:
const [, forceUpdate] = useReducer(c => c + 1, 0);
function listener(newStore) {
forceUpdate();
storeRef.current = newStore;
}To avoid unnecessary renders, the listener can compare specific dependencies before invoking forceUpdate :
const store = useContainer('count'); // component only cares about count
function listener(newStore) {
const newValue = newStore[dep];
const oldValue = storeRef.current[dep];
if (compare(newValue, oldValue)) {
forceUpdate();
}
storeRef.current = newStore;
}This yields a minimal API, clean separation of logic and UI, and precise updates without deep Provider nesting.
zustand
zustand further simplifies the architecture by letting the Store itself expose a setState function. The Hook receives this function, updates the Store directly, and the container notifies listeners.
function useCount(setState) {
const increment = () => setState(state => ({ ...state, count: state.count + 1 }));
const decrement = () => setState(state => ({ ...state, count: state.count - 1 }));
return { count: 0, increment, decrement };
}
function createContainer(hook) {
let store;
const setState = partial => {
const nextStore = partial(store);
if (nextStore !== store) {
store = Object.assign({}, store, nextStore);
onUpdate(store);
}
};
store = hook(setState);
}
const useContainer = createContainer(useCount);All three libraries share the same underlying idea: abstract state logic into a Hook, expose a Store, and provide a lightweight way for components to consume and react to changes.
Summary
The article walks through the evolution of React state‑management solutions, starting from native hooks, moving through unstated‑next , hox , and finally zustand . Each approach addresses the drawbacks of the previous one—reducing boilerplate, avoiding prop‑drilling, eliminating deep Provider nesting, and enabling precise updates. By the end, readers understand how to build or choose a minimal, hook‑based state‑management library for modern React applications.
NetEase Cloud Music Tech Team
Official account of NetEase Cloud Music Tech Team
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.