React useReducer 高级模式

深入理解 useReducer 的高级用法:复杂状态形状、reducer 组合、Context 配合、以及与 Redux 模式的关系。

React useReducer 高级模式

useReducer 基础回顾

useReduceruseState 的替代方案,适用于复杂状态逻辑

const [state, dispatch] = useReducer(reducer, initialState);

// reducer 函数签名
function reducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + 1 };
    case 'DECREMENT':
      return { ...state, count: state.count - 1 };
    default:
      return state;
  }
}

选择 useReducer 而不是 useState 的标准:

  • 状态变化有多个互斥的逻辑分支
  • 状态变化相互依赖,需要基于旧状态计算
  • 状态是复杂对象,多个属性需要联动更新

复杂状态形状的设计

扁平化 vs 嵌套

// 不好的设计:嵌套过深
const state = {
  user: {
    profile: {
      name: 'Alice',
      settings: {
        theme: 'dark',
        notifications: {
          email: true,
          push: false
        }
      }
    }
  }
};

// 扁平化设计:更容易单独更新
const state = {
  user: {
    id: '1',
    name: 'Alice',
  },
  settings: {
    theme: 'dark',
    notifications: {
      email: true,
      push: false
    }
  }
};

扁平化设计的优势在于更新更简单,不需要深层解构:

// 嵌套更新:容易出错
return {
  ...state,
  user: {
    ...state.user,
    profile: {
      ...state.user.profile,
      settings: {
        ...state.user.profile.settings,
        notifications: {
          ...state.user.profile.settings.notifications,
          push: true
        }
      }
    }
  }
};

// 扁平化更新:简单直接
return {
  ...state,
  settings: {
    ...state.settings,
    notifications: {
      ...state.settings.notifications,
      push: true
    }
  }
};

使用 Immer 简化不可变更新

Immer 是一个让你用可变语法写不可变更新的库:

import { produce } from 'immer';

const reducer = (state, action) => {
  switch (action.type) {
    case 'UPDATE_NOTIFICATION':
      return produce(state, draft => {
        // 可以直接修改 draft
        draft.settings.notifications.push = action.value;
      });
    case 'ADD_USER':
      return produce(state, draft => {
        draft.users.push(action.user);
      });
    default:
      return state;
  }
};

Reducer 组合模式

分离 Reducer

当状态很复杂时,可以拆分成多个 reducer:

// 初始状态
const initialState = {
  users: {
    list: [],
    loading: false,
    error: null
  },
  posts: {
    list: [],
    loading: false,
    error: null
  }
};

// 拆分的 reducer
function usersReducer(state, action) {
  switch (action.type) {
    case 'USERS_LOADING':
      return { ...state, loading: true };
    case 'USERS_SUCCESS':
      return { loading: false, list: action.users };
    case 'USERS_ERROR':
      return { loading: false, error: action.error };
    default:
      return state;
  }
}

function postsReducer(state, action) {
  // ...
}

// 组合 reducer
function combinedReducer(state, action) {
  return {
    users: usersReducer(state.users, action),
    posts: postsReducer(state.posts, action)
  };
}

function App() {
  const [state, dispatch] = useReducer(combinedReducer, initialState);
}

使用 combineReducers( Redux 风格)

import { combineReducers } from 'redux';  // 或者自己实现

const usersReducer = (state = { list: [] }, action) => {
  switch (action.type) {
    case 'ADD_USER':
      return { ...state, list: [...state.list, action.user] };
    default:
      return state;
  }
};

const postsReducer = (state = { list: [] }, action) => {
  switch (action.type) {
    case 'ADD_POST':
      return { ...state, list: [...state.list, action.post] };
    default:
      return state;
  }
};

const rootReducer = combineReducers({
  users: usersReducer,
  posts: postsReducer
});

Action Creator 模式

基础 Action Creator

// 简单的 action creator
const addTodo = (text) => ({
  type: 'ADD_TODO',
  payload: { id: Date.now(), text, done: false }
});

const toggleTodo = (id) => ({
  type: 'TOGGLE_TODO',
  payload: { id }
});

const deleteTodo = (id) => ({
  type: 'DELETE_TODO',
  payload: { id }
});

// 使用
dispatch(addTodo('Learn React'));
dispatch(toggleTodo(1));
dispatch(deleteTodo(1));

带验证的 Action Creator

const addTodo = (text) => {
  if (!text || typeof text !== 'string') {
    return { type: 'ERROR', payload: 'Invalid text' };
  }
  if (text.length > 200) {
    return { type: 'ERROR', payload: 'Text too long' };
  }
  return {
    type: 'ADD_TODO',
    payload: { id: Date.now(), text: text.trim(), done: false }
  };
};

Thunk 模式(异步 Action)

// 异步 action creator(thunk)
const fetchUser = (userId) => {
  return async (dispatch, getState) => {
    dispatch({ type: 'FETCH_USER_REQUEST' });

    try {
      const response = await fetch(`/api/users/${userId}`);
      const user = await response.json();
      dispatch({ type: 'FETCH_USER_SUCCESS', payload: user });
    } catch (error) {
      dispatch({ type: 'FETCH_USER_ERROR', payload: error.message });
    }
  };
};

// 使用
dispatch(fetchUser(123));

Context 配合 Reducer

基础模式

const StateContext = createContext(null);
const DispatchContext = createContext(null);

function Provider({ children }) {
  const [state, dispatch] = useReducer(reducer, initialState);

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

// 使用
function ConsumerComponent() {
  const state = useContext(StateContext);
  const dispatch = useContext(DispatchContext);

  // 或者用自定义 hook 简化
  // const { state, dispatch } = useAppContext();
}

分离 State 和 Dispatch

最佳实践是分离 state 和 dispatch 的 Context

const StateContext = createContext(null);
const DispatchContext = createContext(null);

// 分离的好处:
// 1. dispatch 函数永远稳定,不需要是 Context 的值
// 2. 可以单独订阅 state 的特定部分
// 3. 更容易进行性能优化

带选择的 Context 消费

function UserProfile() {
  // 只订阅 user 部分,不关心其他 state
  const user = useContextSelector(StateContext, s => s.user);
  // 或
  const { user } = useAppState();
}

状态范式化

范式化数据结构

范式化(Normalizing)是数据库概念,指将嵌套数据拆分成表/实体:

// 非范式化
const state = {
  posts: [
    {
      id: 1,
      title: 'Hello',
      author: { id: 1, name: 'Alice' },
      comments: [
        { id: 1, author: { id: 2, name: 'Bob' }, text: 'Great!' }
      ]
    }
  ]
};

// 范式化
const state = {
  posts: {
    byId: {
      1: { id: 1, title: 'Hello', authorId: 1, commentIds: [1] }
    },
    allIds: [1]
  },
  users: {
    byId: {
      1: { id: 1, name: 'Alice' },
      2: { id: 2, name: 'Bob' }
    },
    allIds: [1, 2]
  },
  comments: {
    byId: {
      1: { id: 1, authorId: 2, text: 'Great!' }
    },
    allIds: [1]
  }
};

范式化的优势

  1. 更新简单:修改用户信息只需要更新一个地方
  2. 查找高效:不需要遍历数组
  3. 避免重复:数据只存储一份

创建 Selector

// 获取单个 post
const selectPostById = (state, postId) => state.posts.byId[postId];

// 获取带展开作者的 post
const selectPostWithAuthor = (state, postId) => {
  const post = state.posts.byId[postId];
  if (!post) return null;
  const author = state.users.byId[post.authorId];
  return { ...post, author };
};

// 获取所有 posts
const selectAllPosts = (state) =>
  state.posts.allIds.map(id => selectPostById(state, id));

// 使用
function PostList() {
  const posts = useAppState(state => selectAllPosts(state));
  // 或
  const post = useAppState(state => selectPostById(state, 123));
}

useReducer 与 useState 的选择

useReducer 的优势场景

  1. 复杂的状态变化逻辑:多个 action 类型,相互依赖
  2. 状态变化需要触发副作用:如日志记录、持久化
  3. 状态变化需要被撤销/重做:reducer 可以记录历史
  4. 组件树中多个组件需要访问状态:配合 Context

useState 的优势场景

  1. 简单、独立的状态:布尔值、字符串、数字
  2. 状态变化简单直接:直接赋值,不需要计算
  3. 状态之间没有关联:一个状态变化不影响另一个

混用模式

function ComplexComponent() {
  // useState 用于简单状态
  const [isExpanded, setIsExpanded] = useState(false);

  // useReducer 用于复杂状态
  const [formState, dispatch] = useReducer(formReducer, initialFormState);

  return (
    <div>
      <button onClick={() => setIsExpanded(!isExpanded)}>Toggle</button>
      {isExpanded && <Form state={formState} dispatch={dispatch} />}
    </div>
  );
}

面试中的表达

面试中聊到 useReducer,通常是在考察你对复杂状态管理的理解:

useReducer 本质上是把状态变化的逻辑集中到一个纯函数(reducer)中,通过 action 来描述「发生了什么」,而不是直接修改状态。这种模式的好处是:所有状态变化都是可预测的,调试容易,也方便实现撤销/重做。

Reducer 组合和 Context 配合是 React 状态管理的常见模式。拆分 reducer 可以让代码更模块化,配合 Context 可以实现类似 Redux 的全局状态管理,但更轻量。

关于 useReducer vs useState,我的经验是:简单状态用 useState,复杂状态用 useReducer。当状态变化逻辑开始变得复杂,或者需要撤销/重做功能时,就该考虑 useReducer 了。


延展阅读