The React state management landscape has evolved significantly, with lighter alternatives challenging Redux's dominance. Zustand, Jotai, and Valtio represent three distinct approaches to state management, each optimized for different use cases. Understanding their architectural differences helps teams choose the right tool for their specific requirements.
Architectural Philosophy
Each library takes a fundamentally different approach to state management. Zustand uses a centralized store pattern with hooks, Jotai embraces atomic state with bottom-up composition, and Valtio leverages proxy-based mutable updates with immutable snapshots.
Zustand: Simplified Flux
Zustand implements a minimal flux-like pattern without boilerplate. The store is a hook that components can subscribe to, with automatic re-renders when selected state changes. The API surface is deliberately small—create a store, use the hook, update state.
1import create from 'zustand';
2
3interface TodoStore {
4 todos: Todo[];
5 addTodo: (text: string) => void;
6 toggleTodo: (id: string) => void;
7 filter: 'all' | 'active' | 'completed';
8 setFilter: (filter: TodoStore['filter']) => void;
9}
10
11const useTodoStore = create<TodoStore>((set) => ({
12 todos: [],
13 filter: 'all',
14
15 addTodo: (text) => set((state) => ({
16 todos: [...state.todos, { id: crypto.randomUUID(), text, completed: false }]
17 })),
18
19 toggleTodo: (id) => set((state) => ({
20 todos: state.todos.map(todo =>
21 todo.id === id ? { ...todo, completed: !todo.completed } : todo
22 )
23 })),
24
25 setFilter: (filter) => set({ filter })
26}));
27
28// Component usage with selector for optimal re-renders
29function TodoList() {
30 const { todos, filter } = useTodoStore((state) => ({
31 todos: state.todos,
32 filter: state.filter
33 }));
34
35 const filteredTodos = todos.filter(todo => {
36 if (filter === 'active') return !todo.completed;
37 if (filter === 'completed') return todo.completed;
38 return true;
39 });
40
41 return <>{/* render todos */}</>;
42}The selector pattern is critical for performance. Without it, components re-render on any state change. Zustand uses shallow equality comparison by default, so returning new objects from selectors can cause unnecessary renders.
Jotai: Atomic State Composition
Jotai's atom-based model breaks state into minimal units that compose together. Each atom is an independent piece of state with its own subscribers. Derived atoms automatically recompute when dependencies change, similar to computed values in MobX or signals in SolidJS.
1import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai';
2
3// Primitive atoms
4const todosAtom = atom<Todo[]>([]);
5const filterAtom = atom<'all' | 'active' | 'completed'>('all');
6
7// Derived atom with automatic dependency tracking
8const filteredTodosAtom = atom((get) => {
9 const todos = get(todosAtom);
10 const filter = get(filterAtom);
11
12 if (filter === 'active') return todos.filter(t => !t.completed);
13 if (filter === 'completed') return todos.filter(t => t.completed);
14 return todos;
15});
16
17// Write-only atom for actions
18const addTodoAtom = atom(
19 null,
20 (get, set, text: string) => {
21 const todos = get(todosAtom);
22 set(todosAtom, [...todos, {
23 id: crypto.randomUUID(),
24 text,
25 completed: false
26 }]);
27 }
28);
29
30function TodoList() {
31 // Only re-renders when filtered list changes
32 const filteredTodos = useAtomValue(filteredTodosAtom);
33 const addTodo = useSetAtom(addTodoAtom);
34
35 return <>{/* render todos */}</>;
36}The atomic model excels at fine-grained reactivity. Components subscribe only to the atoms they use, eliminating manual selector optimization. Derived atoms form a dependency graph that automatically updates when source atoms change.
Valtio: Mutable API with Immutable Benefits
Valtio uses ES6 Proxies to track mutations and convert them to immutable updates. Developers write mutable-style code, but the library ensures immutable state updates under the hood. This approach feels natural for developers coming from Vue or MobX.
1import { proxy, useSnapshot } from 'valtio';
2
3interface TodoState {
4 todos: Todo[];
5 filter: 'all' | 'active' | 'completed';
6 addTodo: (text: string) => void;
7 toggleTodo: (id: string) => void;
8}
9
10const todoState = proxy<TodoState>({
11 todos: [],
12 filter: 'all',
13
14 addTodo(text) {
15 // Direct mutation - Valtio converts to immutable update
16 this.todos.push({
17 id: crypto.randomUUID(),
18 text,
19 completed: false
20 });
21 },
22
23 toggleTodo(id) {
24 const todo = this.todos.find(t => t.id === id);
25 if (todo) {
26 todo.completed = !todo.completed;
27 }
28 }
29});
30
31function TodoList() {
32 // useSnapshot creates immutable snapshot for React
33 const snap = useSnapshot(todoState);
34
35 const filteredTodos = snap.todos.filter(todo => {
36 if (snap.filter === 'active') return !todo.completed;
37 if (snap.filter === 'completed') return todo.completed;
38 return true;
39 });
40
41 return <>{/* render todos */}</>;
42}The proxy tracks which properties components access during render. Only components accessing changed properties re-render. This automatic tracking eliminates selector functions entirely, though it requires understanding how proxy-based reactivity works.
Performance Characteristics
Bundle size matters for initial load performance. Zustand weighs approximately 1.1KB gzipped, Jotai around 2.9KB, and Valtio roughly 3.5KB. These differences are minimal in most applications, but can matter in bundle-size-constrained environments.
Runtime performance depends heavily on usage patterns. Zustand requires manual selector optimization to prevent unnecessary renders. A poorly written selector that returns new objects on every call defeats the optimization entirely:
1// Anti-pattern: Creates new object every render
2const data = useTodoStore((state) => ({
3 todos: state.todos.filter(t => !t.completed)
4}));
5
6// Better: Use shallow equality or split selectors
7const todos = useTodoStore((state) => state.todos);
8const activeOnly = todos.filter(t => !t.completed);Advertisement
Jotai's atomic model provides automatic optimization. Components re-render only when atoms they read change. The trade-off is more atoms to manage and understand dependency relationships between them.
Valtio's proxy-based tracking offers the best developer experience for fine-grained updates but can be harder to debug. Proxy behavior isn't always intuitive, especially with nested objects or arrays.
Real-World Use Cases
Zustand works well for applications with clear state boundaries and predictable update patterns. E-commerce applications, dashboards, and form-heavy interfaces benefit from its straightforward API. The middleware ecosystem (persist, devtools, immer) handles common requirements without additional dependencies.
Jotai excels in applications requiring complex derived state or real-time updates. Consider a collaborative editor where multiple users edit different parts of a document. Each section can be an atom, with derived atoms computing aggregate state. Components subscribe only to relevant atoms, minimizing re-renders as users type.
1// Collaborative editor example
2const documentSectionsAtom = atom<Map<string, Section>>(new Map());
3const userCursorsAtom = atom<Map<string, CursorPosition>>(new Map());
4
5// Derived atom for specific section
6const sectionAtom = (sectionId: string) => atom(
7 (get) => get(documentSectionsAtom).get(sectionId),
8 (get, set, newContent: string) => {
9 const sections = new Map(get(documentSectionsAtom));
10 sections.set(sectionId, { ...sections.get(sectionId)!, content: newContent });
11 set(documentSectionsAtom, sections);
12 }
13);Valtio fits applications with deeply nested state or frequent small updates. Configuration panels, game state, or complex form wizards benefit from the mutable API. The ability to mutate nested properties directly reduces boilerplate:
1// Deep update without spread operators
2const configState = proxy({
3 ui: {
4 theme: {
5 colors: {
6 primary: '#007bff',
7 secondary: '#6c757d'
8 }
9 }
10 }
11});
12
13// Simple mutation
14configState.ui.theme.colors.primary = '#0056b3';Integration and Ecosystem
All three libraries integrate cleanly with React 18's concurrent features. Zustand supports React 18 out of the box with proper use of useSyncExternalStore. Jotai and Valtio both handle concurrent rendering correctly, though Valtio requires React 18+ for optimal performance.
TypeScript support varies. Zustand requires explicit typing for stores but provides excellent inference once set up. Jotai offers strong typing with minimal boilerplate—atoms infer types from initial values. Valtio's proxy-based approach can challenge TypeScript, especially with complex nested structures.
DevTools integration exists for all three. Zustand's Redux DevTools middleware provides time-travel debugging. Jotai offers a DevTools package with atom inspection. Valtio includes built-in DevTools support showing proxy state changes.
Server-side rendering requires different approaches. Zustand needs manual hydration of initial state. Jotai provides a Provider component for SSR with proper hydration. Valtio supports SSR but requires careful handling of proxy initialization on the server.
Making the Choice
Choose Zustand for applications needing a Redux-like pattern without boilerplate. Teams familiar with Redux will find the mental model comfortable. The middleware ecosystem handles persistence, logging, and other cross-cutting concerns.
Choose Jotai when fine-grained reactivity matters and the application has complex derived state. The atomic model scales well as applications grow, and the bottom-up composition prevents tight coupling between state pieces.
Choose Valtio when developer experience and rapid iteration are priorities. The mutable API reduces cognitive load, especially for developers from Vue or Angular backgrounds. Be prepared to understand proxy behavior and potential edge cases.
None of these libraries locks teams into specific patterns. Applications can mix approaches—using Zustand for global app state while Jotai manages form state, for example. The lightweight nature of all three makes experimentation low-risk.
The React state management ecosystem continues evolving. These libraries represent current best practices, but requirements change. Evaluate based on team familiarity, application complexity, and performance needs rather than popularity metrics alone.






