前端状态管理折腾手记
这次做前端状态管理改造,从 Redux 到 Zustand,。
下面按我实际碰到的顺序记。
最初的问题
Props Drilling
// Props Drilling:数据层层传递
function App() {
return (
<div>
<Header user={user} />
<Main user={user} />
<Footer user={user} />
</div>
);
}
function Header({ user }) {
return (
<header>
<UserMenu user={user} />
</header>
);
}
function UserMenu({ user }) {
return (
<div>
<span>{user.name}</span>
<Avatar user={user} />
</div>
);
}
function Avatar({ user }) {
return <img src={user.avatar} alt={user.name} />;
}
// 问题:
// 1. 组件层级深,传递麻烦
// 2. 中间组件不需要数据也要传递
// 3. 难以管理共享状态
Redux
基础 Redux
// Action Types
const SET_USER = 'SET_USER';
const ADD_TODO = 'ADD_TODO';
const REMOVE_TODO = 'REMOVE_TODO';
// Action Creators
const setUser = (user) => ({ type: SET_USER, payload: user });
const addTodo = (todo) => ({ type: ADD_TODO, payload: todo });
const removeTodo = (id) => ({ type: REMOVE_TODO, payload: id });
// Initial State
const initialState = {
user: null,
todos: []
};
// Reducer
function rootReducer(state = initialState, action) {
switch (action.type) {
case SET_USER:
return { ...state, user: action.payload };
case ADD_TODO:
return { ...state, todos: [...state.todos, action.payload] };
case REMOVE_TODO:
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.payload)
};
default:
return state;
}
}
// Store
import { createStore } from 'redux';
const store = createStore(rootReducer);
// 使用 Redux
import { Provider, useSelector, useDispatch } from 'react-redux';
function App() {
return (
<Provider store={store}>
<Counter />
<TodoList />
</Provider>
);
}
function UserComponent() {
const user = useSelector(state => state.user);
const dispatch = useDispatch();
return (
<div>
<p>{user?.name}</p>
<button onClick={() => dispatch(setUser({ name: 'Alice' }))}>
Set User
</button>
</div>
);
}
Redux Toolkit
// 使用 Redux Toolkit 简化配置
import { createSlice, configureStore } from '@reduxjs/toolkit';
// 创建 Slice
const userSlice = createSlice({
name: 'user',
initialState: null,
reducers: {
setUser: (state, action) => action.payload,
clearUser: () => null
}
});
const todoSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
addTodo: (state, action) => {
state.push(action.payload);
},
removeTodo: (state, action) => {
return state.filter(todo => todo.id !== action.payload);
}
}
});
// 导出 Actions
export const { setUser, clearUser } = userSlice.actions;
export const { addTodo, removeTodo } = todoSlice.actions;
// 创建 Store
const store = configureStore({
reducer: {
user: userSlice.reducer,
todos: todoSlice.reducer
}
});
// 使用 Redux Toolkit
function UserComponent() {
const user = useSelector(state => state.user);
const dispatch = useDispatch();
return (
<div>
<p>{user?.name}</p>
<button onClick={() => dispatch(setUser({ name: 'Alice' }))}>
Set User
</button>
</div>
);
}
Zustand
基础 Zustand
// 创建 Store
import create from 'zustand';
const useStore = create((set) => ({
user: null,
todos: [],
setUser: (user) => set({ user }),
addTodo: (todo) => set((state) => ({ todos: [...state.todos, todo] })),
removeTodo: (id) => set((state) => ({
todos: state.todos.filter(todo => todo.id !== id)
}))
}));
// 使用 Zustand
function UserComponent() {
const user = useStore(state => state.user);
const setUser = useStore(state => state.setUser);
return (
<div>
<p>{user?.name}</p>
<button onClick={() => setUser({ name: 'Alice' })}>
Set User
</button>
</div>
);
}
中间件
// 添加中间件
import { devtools, persist } from 'zustand/middleware';
const useStore = create(
devtools(
persist(
(set) => ({
user: null,
todos: [],
setUser: (user) => set({ user }),
addTodo: (todo) => set((state) => ({ todos: [...state.todos, todo] })),
removeTodo: (id) => set((state) => ({
todos: state.todos.filter(todo => todo.id !== id)
}))
}),
{
name: 'my-storage', // LocalStorage key
}
)
)
);
异步操作
// 处理异步操作
const useStore = create((set, get) => ({
user: null,
loading: false,
error: null,
fetchUser: async (id) => {
set({ loading: true, error: null });
try {
const response = await fetch(`/api/users/${id}`);
const user = await response.json();
set({ user, loading: false });
} catch (error) {
set({ error: error.message, loading: false });
}
}
}));
// 使用异步操作
function UserComponent({ userId }) {
const { user, loading, error, fetchUser } = useStore();
useEffect(() => {
fetchUser(userId);
}, [userId]);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return <p>{user?.name}</p>;
}
踩过的坑
坑一:状态更新不及时
组件没有及时响应状态更新。
解决:确保正确使用 immer 或不可变更新。
// Zustand 使用 immer 进行不可变更新
const useStore = create((set) => ({
todos: [],
updateTodo: (id, updates) => set((state) => ({
todos: state.todos.map(todo =>
todo.id === id ? { ...todo, ...updates } : todo
)
}))
}));
// 或使用 immer 中间件
import { immer } from 'zustand/middleware/immer';
const useStore = create(
immer((set) => ({
todos: [],
updateTodo: (id, updates) => set((state) => {
const todo = state.todos.find(t => t.id === id);
if (todo) {
Object.assign(todo, updates);
}
})
}))
);
坑二:过度使用状态
将所有数据都放在状态中,导致状态管理复杂。
解决:合理使用状态,区分持久化状态和临时状态。
// 使用 Context API 处理临时状态
const ThemeContext = createContext();
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
// 使用 Zustand 处理持久化状态
const useStore = create((set) => ({
user: null,
setUser: (user) => set({ user })
}));
坑三:状态粒度太细
状态粒度太细,导致更新频繁。
解决:合理划分状态粒度。
// 细粒度状态(不推荐)
const useStore = create((set) => ({
firstName: '',
lastName: '',
email: '',
setFirstName: (firstName) => set({ firstName }),
setLastName: (lastName) => set({ lastName }),
setEmail: (email) => set({ email })
}));
// 粗粒度状态(推荐)
const useStore = create((set) => ({
user: {
firstName: '',
lastName: '',
email: ''
},
setUser: (user) => set({ user })
}));
状态管理选择
比较不同方案
| 方案 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|
| Context API | 内置,简单 | 性能差,嵌套问题 | 简单状态 |
| Redux | 生态成熟,规范 | 代码量大,学习曲线 | 大型应用 |
| Redux Toolkit | 简化配置 | 仍需较多代码 | 中大型应用 |
| Zustand | 简单,轻量 | 生态相对较小 | 各种规模 |
| Recoil | Facebook 出品 | 复杂度高 | 大型应用 |
选择建议
小型项目:Context API 或 Zustand
中型项目:Zustand 或 Redux Toolkit
大型项目:Redux Toolkit 或 Recoil
性能敏感项目:Zustand 或 Jotai
写在最后
状态管理这东西,不只是技术,是架构设计。
解决了:
- 数据共享
- 状态一致性
- 组件解耦
带来了:
- 复杂度增加
- 学习成本
- 调试困难
选型之前先评估:
- 应用规模
- 团队能力
- 性能要求
- 生态需求
不是所有应用都需要复杂的状态管理,有时候 Context API 就够用。
这次前端状态管理改造花了一个月,从 Redux 到 Zustand。改造完成后,代码量减少了 60%,性能提升了 30%。
版权声明: 本文首发于 指尖魔法屋-前端状态管理折腾手记(https://blog.thinkmoon.cn/post/91-frontend-state-management-redux-zustand-practice/) 转载或引用必须申明原指尖魔法屋来源及源地址!
评论
使用 GitHub 账号登录后即可留言,支持 Markdown。