前端状态管理折腾手记

这次做前端状态管理改造,从 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简单,轻量生态相对较小各种规模
RecoilFacebook 出品复杂度高大型应用

选择建议

小型项目: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/) 转载或引用必须申明原指尖魔法屋来源及源地址!