Frontend Development 21 min read

State Management in React: From Redux to Unstated‑Next, Constate, Zustand, Jotai and Valtio

This article reviews the evolution of React state‑management solutions, comparing classic Redux with modern Hook‑based libraries such as Unstated‑Next, Constate, Zustand, Jotai and Valtio, explains core concepts like algebraic effects, context duplication, and provides practical code examples and performance tips.

LOFTER Tech Team
LOFTER Tech Team
LOFTER Tech Team
State Management in React: From Redux to Unstated‑Next, Constate, Zustand, Jotai and Valtio

Preface

Before React Hooks, Redux was the de‑facto solution for centralized state management, offering predictability and a rich ecosystem, but it introduced considerable boilerplate and complexity, especially in small projects.

React Hooks bring a simpler API (e.g., useContext ) and enable libraries that mimic algebraic effects, leading to a new generation of state‑management tools, including a Hook‑based API in React‑Redux and the introduction of redux‑toolkit .

Algebraic Effects (Extension)

Algebraic effects separate side‑effects from function calls, allowing what and how to be handled independently. The following example demonstrates a pure function that calculates a total picture count without caring about the implementation of getPicNum :

function getTotalPicNum(user1, user2) {
  const num1 = getPicNum(user1);
  const num2 = getPicNum(user2);
  return picNum1 + picNum2;
}

When getPicNum becomes asynchronous, the function must also become async, illustrating the "contagious" nature of async/await :

async function getTotalPicNum(user1, user2) {
  const num1 = await getPicNum(user1);
  const num2 = await getPicNum(user2);
  return picNum1 + picNum2;
}

With a hypothetical perform syntax, the effect can pause execution, fetch the needed value, and resume automatically, similar to how React Hooks encapsulate effect handling.

Why This Article Exists

I discovered unstated‑next , a lightweight library that uses useContext to provide basic state management. It inspired a deeper look at the current mainstream state‑management libraries.

State Basics

In the jQuery era, JavaScript code mixed DOM manipulation with business logic, resulting in tangled "spaghetti" code. Modern frameworks promote a data‑driven view, where state is the single source of truth.

Class components store state in this.state , while function components use useState or useReducer . To avoid complexity, components are split and communicate via props , following a unidirectional data flow.

Redux

Core concepts:

Store – a single container for the whole application state.

State – a snapshot of the store at a given moment.

Action – a plain object describing a state change, dispatched by the view.

Action Creator – functions that generate actions.

Reducer – a pure function that receives the current state and an action, returning a new state.

dispatch – the only way for the view to trigger a state change.

Example implementation (simplified):

// action.js
export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';
export const RESET = 'RESET';
export function increaseCount() { return { type: INCREMENT }; }
export function decreaseCount() { return { type: DECREMENT }; }
export function resetCount() { return { type: RESET }; }

// reducer.js
const INITIAL_STATE = { count: 0, history: [] };
function handleChange(state, change) {
  const { count, history } = state;
  return { count: count + change, history: [count + change, ...history] };
}
export default function counter(state = INITIAL_STATE, action) {
  switch (action.type) {
    case INCREMENT: return handleChange(state, 1);
    case DECREMENT: return handleChange(state, -1);
    case RESET: return INITIAL_STATE;
    default: return state;
  }
}

// store.js
export const CounterStore = createStore(reducer);

Redux‑Toolkit simplifies the boilerplate by providing createSlice and auto‑generated action creators.

Using Context for State Management

Example of exposing a count state via a custom context:

const Foo = () => {
  const [count, setCount] = React.useState(0);
  return (<>
{count}
setCount(v => v + 1)}>Click Me!
);
};

const CountContext = React.createContext(null);
const CountProvider = ({ children }) => {
  const [value] = React.useState(0);
  return (
{children}
);
};

const Foo = () => {
  const count = React.useContext(CountContext);
  return
{count}
;
};

const Bar = () => {
  const count = React.useContext(CountContext);
  return
{count % 2 ? 'Even' : 'Odd'}
;
};

const App = () => (
);

Elevating the provider to the top of the component tree shares state globally, but introduces challenges when the state needs to be mutated from child components.

Unstated‑Next

Provides two APIs – createContainer and useContainer – to encapsulate state and its mutators in a reusable container.

import React, { useState } from "react";
import { createContainer } from "unstated-next";

function useCounter(initialState = 0) {
  let [count, setCount] = useState(initialState);
  let decrement = () => setCount(count - 1);
  let increment = () => setCount(count + 1);
  return { count, decrement, increment };
}

let Counter = createContainer(useCounter);

function CounterDisplay() {
  let counter = Counter.useContainer();
  return (
-
{counter.count}
+
);
}

function App() {
  return (
);
}

Issues: provider lacks displayName for DevTools, and excessive nesting can cause performance problems.

Constate

Improves on Unstated‑Next by adding displayName and a selectors API for fine‑grained context consumption.

import React, { useState, useCallback } from "react";
import constate from "constate";

function useCounter() {
  const [count, setCount] = useState(0);
  const increment = useCallback(() => setCount(prev => prev + 1), []);
  return { count, increment };
}

const [Provider, useCount, useIncrement] = constate(
  useCounter,
  v => v.count,      // useCount
  v => v.increment   // useIncrement
);

function Button() {
  const increment = useIncrement();
  return
+
;
}

function Count() {
  const count = useCount();
  return
{count}
;
}

Zustand

A minimalist global store based on the observer pattern, requiring no Provider and working outside of React as well.

import create from 'zustand';

const useStore = create(set => ({
  bears: 0,
  increasePopulation: () => set(state => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 })
}));

function BearCounter() {
  const bears = useStore(state => state.bears);
  return
{bears} around here ...
;
}

function Controls() {
  const increasePopulation = useStore(state => state.increasePopulation);
  return
one up
;
}

Core implementation includes setState , getState , subscribe , and middleware support (e.g., persistence).

Jotai

Another global state solution built by the author of Zustand, inspired by Recoil's atom concept.

import { atom } from 'jotai';

const countAtom = atom(0);
const countryAtom = atom('Japan');

function Counter() {
  const [count, setCount] = useAtom(countAtom);
  return (
{count}
setCount(c => c + 1)}>one up
);
}

Atoms can be derived, and the library also supports a Provider for scoped stores.

Valtio

Uses Proxy and Reflect to create mutable state objects that automatically trigger component updates.

import { proxy, useSnapshot } from 'valtio';

const state = proxy({ count: 0, text: 'hello' });

function Counter() {
  const snap = useSnapshot(state);
  return (
{snap.count}
++state.count}>+1
);
}

Summary

State‑management solutions revolve around two core ideas: a data source and data consumption. Context‑based approaches provide local state, while libraries like Zustand, Jotai, and Valtio offer global state with different mutation models (immutable vs. mutable). Understanding their underlying mechanisms—observer pattern for immutable data and Proxy for mutable data—helps developers choose the right tool for their React applications.

Reduxstate managementreactHooksZustandContext API
LOFTER Tech Team
Written by

LOFTER Tech Team

Technical sharing and discussion from NetEase LOFTER Tech Team

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.