0%

Zustand 快速入门——React 状态管理最佳实践

今天在重构老项目的时候突然想起之前研究过的 zustand,小巧精悍的状态管理库。相比 Redux 那一套复杂的模板代码,zustand 真的是让人眼前一亮!

什么是 zustand?

  zustand(德语中的”state”)是一个小型、快速且可扩展的状态管理库,适用于 React 应用。由 Poimandres 团队开发,zustand 以其极简的 API 和无需包装器组件的设计理念受到开发者的喜爱。与 Redux、MobX 等传统状态管理库相比,zustand 提供了更加直观和简洁的开发体验。

  zustand 的核心理念是提供最小化的 API,让开发者专注于业务逻辑而非复杂的配置。它支持中间件、SSR、Typescript,并且包体积只有2KB 左右,是现代 React 应用状态管理的理想选择之一。

zustand 的主要特点

  1. 极简 API: 不需要 Provider 包装,也不需要复杂的配置
  2. 无模板代码: 相比 Redux 的 action/type/reducer 模式,zustand 几乎没有任何模板代码
  3. 性能优异: 使用原生 ES6 Proxy 进行变更检测,避免不必要的渲染
  4. Typescript 友好: 提供完整的 Typescript 支持
  5. 中间件支持: 支持日志记录、持久化、时间旅行等高级功能

安装和基本使用

安装 zustand

1
2
3
4
5
6
7
8
# 使用 npm
npm install zustand

# 使用 yarn
yarn add zustand

# 使用 pnpm
pnpm add zustand

创建第一个 store

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// stores/useStore.JS
import { create } from 'zustand';

const useStore = create((set, get) => ({
// 状态 count: 0,
user: null,
isLoggedIn: false,

// 动作 increment: () => set(state => ({ count: state.count + 1 })),
decrement: () => set(state => ({ count: state.count - 1 })),
setUser: (user) => set({ user }),
login: (username, password) => set({
user: { username, loginTime: Date.now() },
isLoggedIn: true
}),
logout: () => set({ user: null, isLoggedIn: false })
}));

export default useStore;

在组件中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// components/Counter.jsx
import React from 'React';
import useStore from '../stores/useStore';

function Counter() {
const { count, increment, decrement } = useStore();

return (
<div>
<h2>计数器: {count}</h2>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
}

export default Counter;

核心概念详解

State(状态)

  状态是 store 中最基本的概念。在 zustand 中,状态是普通 Javascript 对象的一个属性。你可以存储任何类型的数据,包括原始值、对象、数组和函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 复杂状态示例 const useAppStore = create(set => ({
userInfo: {
id: null,
name: '',
email: ''
},
preferences: {
theme: 'light',
notifications: true,
language: 'zh-CN'
},
todos: [],
ui: {
loading: false,
error: null
}
}));

Set(状态更新)

  set函数用于更新 store 中的状态。它可以接受一个对象或一个函数。当传入函数时,该函数接收当前状态作为参数,返回要更新的状态部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const useCounterStore = create(set => ({
count: 0,
doubleCount: 0,

increment: () => set(state => ({
count: state.count + 1,
doubleCount: (state.count + 1) * 2
})),

// 批量更新 reset: () => set({ count: 0, doubleCount: 0 }),

// 异步更新 fetchAndSet: async (apiUrl) => {
const response = await fetch(apiUrl);
const data = await response.Json();
set({ count: data.count });
}
}));

Get(状态获取)

  get函数允许你在更新状态时访问当前状态,避免闭包问题。它通常在需要基于当前状态计算新状态时使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const useTodoStore = create((set, get) => ({
todos: [],
completedCount: 0,

addTodo: (text) => set(state => ({
todos: [...state.todos, { id: Date.now(), text, completed: false }]
})),

toggleTodo: (id) => set(state => ({
todos: state.todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
})),

// 使用 get 访问当前状态 updateCompletedCount: () => set(state => ({
completedCount: state.todos.filter(todo => todo.completed).length
}))
}));

高级用法

分离状态和动作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// stores/todoStore.JS
import { create } from 'zustand';

// 动作定义 const createActions = (set, get) => ({
// 添加待办事项 addTodo: (text) => set(state => ({
todos: [...state.todos, {
id: Date.now(),
text,
completed: false,
createdAt: new Date()
}]
})),

// 切换完成状态 toggleTodo: (id) => set(state => ({
todos: state.todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
})),

// 删除待办事项 removeTodo: (id) => set(state => ({
todos: state.todos.filter(todo => todo.id !== id)
})),

// 清除已完成事项 clearCompleted: () => set(state => ({
todos: state.todos.filter(todo => !todo.completed)
}))
});

// 初始状态 const initialState = {
todos: [],
filter: 'all', // all, active, completed
stats: {
total: 0,
completed: 0,
active: 0
}
};

const useTodoStore = create((set, get) => ({
...initialState,
...createActions(set, get)
}));

export default useTodoStore;

计算属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';

const useUserStore = create(
devtools((set, get) => ({
users: [],
currentUser: null,

// 添加用户 addUser: (user) => set(state => ({
users: [...state.users, user]
})),

// 获取计算属性 getActiveUsers: () => {
const { users } = get();
return users.filter(user => user.isActive);
},

getAdminUsers: () => {
const { users } = get();
return users.filter(user => user.role === 'admin');
},

// 计算总数 getTotalUsers: () => {
const { users } = get();
return users.length;
}
}))
);

// 使用计算属性 function UserStats() {
const { getActiveUsers, getAdminUsers, getTotalUsers } = useUserStore();

return (
<div>
<p>总用户数: {getTotalUsers()}</p>
<p>活跃用户: {getActiveUsers().length}</p>
<p>管理员: {getAdminUsers().length}</p>
</div>
);
}

中间件使用

日志中间件

1
2
3
4
5
6
7
8
9
10
11
12
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';

const useLoggerStore = create(
devtools((set, get) => ({
count: 0,
message: 'Hello',

increment: () => set(state => ({ count: state.count + 1 }), false, 'INCREMENT'),
updateMessage: (msg) => set({ message: msg }, false, 'UPDATE_MESSAGE')
}))
);

持久化中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

const usePersistStore = create(
persist(
(set, get) => ({
theme: 'light',
language: 'en',
favorites: [],

setTheme: (theme) => set({ theme }),
setLanguage: (language) => set({ language }),

addToFavorites: (item) => set(state => ({
favorites: [...state.favorites, item]
})),

removeFromFavorites: (id) => set(state => ({
favorites: state.favorites.filter(fav => fav.id !== id)
}))
}),
{
name: 'app-storage', // 存储键名 partialize: (state) => ({
theme: state.theme,
language: state.language
}) // 只持久化部分状态
}
)
);

自定义中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 自定义节流中间件 const throttle = (config, delay = 1000) => (set, get, API) => {
let lastCall = 0;
return config(
(...args) => {
const now = Date.now();
if (now - lastCall >= delay) {
lastCall = now;
set(...args);
}
},
get,
API
);
};

const useThrottledStore = create(
throttle((set) => ({
value: 0,
updateValue: (newValue) => set({ value: newValue })
}), 1000)
);

与其它状态管理库的对比

zustand vs Redux

特性zustandRedux
安装包大小~2KB~2.5KB (core) + middleware
设置复杂度极简需要 configureStore 等配置
模板代码几乎没有需要 actions/types/reducers
React 集成无需 Provider需要 Provider 包装
Typescript 支持优秀需要额外配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// zustand
import { create } from 'zustand';

const useStore = create(set => ({
count: 0,
increment: () => set(state => ({ count: state.count + 1 }))
}));

// Redux Toolkit (现在的推荐方式)
import { createSlice, configureStore } from '@reduxjs/toolkit';

const counterSlice = createSlice({
name: 'counter',
initialState: { count: 0 },
reducers: {
increment: (state) => {
state.count += 1;
}
}
});

export const store = configureStore({
reducer: {
counter: counterSlice.reducer
}
});

export const { increment } = counterSlice.actions;

zustand vs Context API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// Context API 需要 Provider 包装
// context/CounterContext.JS
import { createContext, useContext, useReducer } from 'React';

const CounterContext = createContext();

const counterReducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
case 'DECREMENT':
return { ...state, count: state.count - 1 };
default:
return state;
}
};

export const CounterProvider = ({ children }) => {
const [state, dispatch] = useReducer(counterReducer, { count: 0 });

return (
<CounterContext.Provider value={{ state, dispatch }}>
{children}
</CounterContext.Provider>
);
};

export const useCounter = () => {
const context = useContext(CounterContext);
if (!context) {
throw new Error('useCounter must be used within CounterProvider');
}
return context;
};

// zustand 无需 Provider
import { create } from 'zustand';

const useCounterStore = create(set => ({
count: 0,
increment: () => set(state => ({ count: state.count + 1 }))
}));

实际应用场景

用户认证状态管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// stores/authStore.JS
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

const useAuthStore = create(
persist(
(set, get) => ({
user: null,
token: null,
isAuthenticated: false,
loading: false,
error: null,

// 登录 login: async (credentials) => {
set({ loading: true, error: null });

try {
const response = await fetch('/API/login', {
method: 'POST',
headers: { 'Content-Type': 'application/Json' },
body: Json.stringify(credentials)
});

const data = await response.Json();

if (response.ok) {
set({
user: data.user,
token: data.token,
isAuthenticated: true,
loading: false
});

// 可以在这里设置请求拦截器 localStorage.setItem('authToken', data.token);
} else {
set({ error: data.message, loading: false });
}
} catch (error) {
set({ error: error.message, loading: false });
}
},

// 注销 logout: () => {
set({
user: null,
token: null,
isAuthenticated: false
});
localStorage.removeItem('authToken');
},

// 更新用户信息 updateUser: (userData) => {
set(state => ({
user: { ...state.user, ...userData }
}));
}
}),
{
name: 'auth-storage',
partialize: (state) => ({
user: state.user,
token: state.token,
isAuthenticated: state.isAuthenticated
})
}
)
);

export default useAuthStore;

购物车状态管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
// stores/cartStore.JS
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

const useCartStore = create(
persist(
(set, get) => ({
items: [],
total: 0,
itemCount: 0,

// 添加商品到购物车 addItem: (product) => set((state) => {
const existingItem = state.items.find(item => item.id === product.id);

if (existingItem) {
return {
items: state.items.map(item =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
)
};
} else {
return {
items: [...state.items, { ...product, quantity: 1 }]
};
}
}),

// 更新商品数量 updateQuantity: (productId, quantity) => set((state) => {
if (quantity <= 0) {
return {
items: state.items.filter(item => item.id !== productId)
};
}

return {
items: state.items.map(item =>
item.id === productId
? { ...item, quantity }
: item
)
};
}),

// 移除商品 removeItem: (productId) => set((state) => ({
items: state.items.filter(item => item.id !== productId)
})),

// 清空购物车 clearCart: () => set({ items: [] }),

// 计算总数 calculateTotals: () => {
const { items } = get();
const total = items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
const itemCount = items.reduce((count, item) => count + item.quantity, 0);

set({ total, itemCount });
}
}),
{
name: 'cart-storage',
onRehydrateStorage: () => {
return (state) => {
// 恢复时重新计算总计 state?.calculateTotals();
};
}
}
)
);

// 在组件中使用 function CartSummary() {
const { items, total, itemCount, calculateTotals } = useCartStore();

// 在组件挂载时重新计算 useEffect(() => {
calculateTotals();
}, []);

return (
<div>
<p>商品数量: {itemCount}</p>
<p>总计: ¥{total.toFixed(2)}</p>
</div>
);
}

性能优化技巧

Selectors 优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { create } from 'zustand';

const useStore = create(set => ({
user: { name: 'John', age: 30, email: 'john@example.com' },
posts: [{ id: 1, title: 'Post 1' }, { id: 2, title: 'Post 2' }],
ui: { theme: 'dark', sidebarOpen: false }
}));

// 错误的使用方式 - 每次都会触发重渲染 function UserProfile() {
const user = useStore(state => state.user); // 这里没问题 const theme = useStore(state => state.ui.theme); // 但这里会因为整个 store 变化而重渲染 return <div>...</div>;
}

// 正确的使用方式 - 分别订阅不同的状态部分 function UserProfile() {
const name = useStore(state => state.user.name);
const theme = useStore(state => state.ui.theme);

return <div>...</div>;
}

// 或者使用 Selectors
const selectUser = state => state.user;
const selectTheme = state => state.ui.theme;

function UserProfile() {
const user = useStore(selectUser);
const theme = useStore(selectTheme);

return <div>...</div>;
}

避免不必要的重渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { shallow } from 'zustand/shallow';

function TodoList() {
// 使用 shallow 比较避免对象引用变化导致的重渲染 const { todos, filter } = useTodoStore(state => ({
todos: state.todos,
filter: state.filter
}), shallow);

const filteredTodos = useMemo(() => {
switch(filter) {
case 'active':
return todos.filter(todo => !todo.completed);
case 'completed':
return todos.filter(todo => todo.completed);
default:
return todos;
}
}, [todos, filter]);

return (
<div>
{filteredTodos.map(todo => <TodoItem key={todo.id} todo={todo} />)}
</div>
);
}

分离高频更新状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 将频繁变化的状态分离到独立的 store 中 const useUIStore = create(set => ({
// 高频更新的状态 loading: false,
notifications: [],
modalOpen: false,

setLoading: (loading) => set({ loading }),
addNotification: (notification) => set(state => ({
notifications: [...state.notifications, notification]
}))
}));

const useAppStore = create(set => ({
// 低频更新的状态 user: null,
settings: {},

setUser: (user) => set({ user }),
updateSettings: (settings) => set(state => ({
settings: { ...state.settings, ...settings }
}))
}));

最佳实践

1. 合理划分 store

1
2
3
4
5
6
7
8
9
10
11
// 好的做法:按功能模块划分 store
// stores/userStore.JS
export const useUserStore = create(/* ... */);

// stores/appStore.JS
export const useAppStore = create(/* ... */);

// stores/uiStore.JS
export const useUIStore = create(/* ... */);

// 避免:所有状态都放在一个 store 中

2. 类型定义(Typescript)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
interface User {
id: number;
name: string;
email: string;
}

interface AuthState {
user: User | null;
token: string | null;
isAuthenticated: boolean;
login: (credentials: { email: string; password: string }) => Promise<void>;
logout: () => void;
}

const useAuthStore = create<AuthState>()(
devtools(
persist(
(set) => ({
user: null,
token: null,
isAuthenticated: false,

login: async (credentials) => {
// 实现登录逻辑
},

logout: () => {
set({ user: null, token: null, isAuthenticated: false });
}
}),
{ name: 'auth-storage' }
)
)
);

3. 错误处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const useErrorStore = create(set => ({
errors: [],

addError: (error) => set(state => ({
errors: [...state.errors, {
id: Date.now(),
message: error.message,
timestamp: new Date(),
severity: error.severity || 'error'
}]
})),

removeError: (id) => set(state => ({
errors: state.errors.filter(error => error.id !== id)
})),

clearErrors: () => set({ errors: [] })
}));

4. 状态初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 在应用启动时初始化状态 import { useAuthStore } from './stores/authStore';

function App() {
const initializeApp = useCallback(async () => {
// 检查是否有持久化的 token,自动登录 const token = localStorage.getItem('authToken');
if (token) {
try {
const response = await fetch('/API/me', {
headers: { Authorization: `Bearer ${token}` }
});
if (response.ok) {
const user = await response.Json();
useAuthStore.setState({
user,
token,
isAuthenticated: true
});
}
} catch (error) {
// 令牌无效,清除本地存储 localStorage.removeItem('authToken');
}
}
}, []);

useEffect(() => {
initializeApp();
}, [initializeApp]);

return (
<div className="app">
{/* 应用内容 */}
</div>
);
}

测试 zustand store

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// __tests__/counterStore.test.JS
import { create } from 'zustand';
import { act } from 'React-dom/test-utils';

// 创建测试用的 store 实例 const createTestStore = () => create(set => ({
count: 0,
increment: () => set(state => ({ count: state.count + 1 })),
decrement: () => set(state => ({ count: state.count - 1 })),
reset: () => set({ count: 0 })
}));

describe('counter store', () => {
let useStore;

beforeEach(() => {
useStore = createTestStore();
});

test('初始状态为0', () => {
const state = useStore.getState();
expect(state.count).toBe(0);
});

test('increment 增加计数', () => {
const { increment } = useStore.getState();
act(() => {
increment();
});
expect(useStore.getState().count).toBe(1);
});

test('decrement 减少计数', () => {
const { decrement } = useStore.getState();
act(() => {
useStore.setState({ count: 5 });
decrement();
});
expect(useStore.getState().count).toBe(4);
});

test('reset 重置计数', () => {
const { increment, reset } = useStore.getState();
act(() => {
increment();
increment();
reset();
});
expect(useStore.getState().count).toBe(0);
});
});

总结

  • zustand 提供了极简的 API 和优秀的开发体验
  • 无需 Provider 包装,降低了组件树的复杂性
  • 支持中间件,可以轻松添加日志、持久化等功能
  • 与 React 生态完美集成,Typescript 支持完善
  • 适合中小型项目的状态管理需求
  • 对于大型复杂应用,仍需考虑其他方案的权衡

用 zustand 重构完项目后,感觉整个代码清爽了不少。再也不用写那些冗长的 action creators 了,代码量减少了近30%,维护起来也方便多了。有时候技术选型真的很重要,选择合适的工具能让开发变得愉快许多!

扩展阅读

  • Zustand 官方文档
  • React 状态管理最佳实践
  • 现代 React 状态管理对比
  • Typescript 与状态管理

参考资料

  • Zustand GitHub Repository: https://github.com/pmndrs/zustand
  • React Documentation: https://reactjs.org/
  • State Management Patterns in React: https://frontendmasters.com/guides/redux-book/
bulb