受够了useState的逻辑分散?来,试试用reducer聚合逻辑

640 阅读4分钟

useState的缺点

经常写react 的同学都知道,useState 是 React 中的一个 Hook,可以在函数组件中管理组件的状态,并在状态更新时重新渲染组件。

这东西虽然简单好用,但有一个致命缺点:当组件有非常多的状态更新逻辑时,事件处理会非常分散,维护起来很头疼!

比如,一个简单的记事本功能

我需要通过三个不同的事件处理程序来实现任务的添加、删除和修改:

const [tasks, setTasks] = useState([  
    { id: 1, title: '去贝加尔湖旅行', completed: false },  
    { id: 2, title: '去烟台海边度假', completed: false },  
    { id: 3, title: '再去一次厦门看海', completed: false },  
]);

// 添加  
const addTask = (taskTitle) => {    
  const newId = tasks.length + 1;  
  const newTask = { id: newId, title: taskTitle, completed: false };  
  setTasks([...tasks, newTask]);  
};  

// 删除  
const deleteTask = (taskId) => {  
  setTasks(tasks.filter(task => task.id !== taskId));  
};  

// 编辑  
const toggleTaskCompletion = (taskId) => {  
  setTasks(tasks.map(task =>   
    task.id === taskId ? { ...task, completed: !task.completed } : task  
  ));  
};  

上面的代码中,每个事件处理程序都通过 setTasks 来更新状态。随着这个组件功能的,其状态逻辑也会越来越多。为了降低这种复杂度,并让所有逻辑都可以存放在一个易于理解的地方,我们可以将这些状态逻辑移到组件之外的一个称为 reducer 的函数中。

使用 reducer 整合状态逻辑

在学习reducer之前,我们先看看使用reducer整合后的代码

import React, { useReducer, useState } from 'react';

// 定义 reducer 函数
const taskReducer = (state, action) => {
  switch (action.type) {
    case 'ADD_TASK':
      const newId = state.length + 1;
      return [...state, { id: newId, title: action.payload, completed: false }];
    case 'DELETE_TASK':
      return state.filter(task => task.id !== action.payload);
    case 'TOGGLE_TASK':
      return state.map(task =>
        task.id === action.payload ? { ...task, completed: !task.completed } : task
      );
    default:
      return state;
  }
};

function TaskList() {
  const [tasks, dispatch] = useReducer(taskReducer, [
    { id: 1, title: '去贝加尔湖旅行', completed: false },
    { id: 2, title: '去烟台海边度假', completed: false },
    { id: 3, title: '再去一次厦门看海', completed: false },
  ]);

  const [newTaskTitle, setNewTaskTitle] = useState('');

  const handleAddTask = () => {
    if (newTaskTitle.trim()) {
      dispatch({ type: 'ADD_TASK', payload: newTaskTitle });
      setNewTaskTitle('');
    }
  };

  return (
    <div>
      <h2>快乐就是哈哈哈的记事本</h2>
      <div>
        <input
          type="text"
          placeholder="添加新任务"
          value={newTaskTitle}
          onChange={(e) => setNewTaskTitle(e.target.value)}
        />
        <button onClick={handleAddTask} style={{ marginLeft: '10px' }}>添加</button>
      </div>
      <div>
        {tasks.map(task => (
          <div key={task.id} style={{ marginTop: '10px' }}>
            <input
              type="checkbox"
              checked={task.completed}
              onChange={() => dispatch({ type: 'TOGGLE_TASK', payload: task.id })}
            />
            {task.title}
            <button onClick={() => { /* Add edit functionality here */ }} style={{ marginLeft: '10px' }}>编辑</button>
            <button onClick={() => dispatch({ type: 'DELETE_TASK', payload: task.id })} style={{ marginLeft: '10px' }}>删除</button>
          </div>
        ))}
      </div>
    </div>
  );
}

export default TaskList;

能够看出,所有的逻辑被整合到taskReducer这个函数中了,我们的逻辑聚合度很高,非常好维护!

useReducer的基本语法

const [state, dispatch] = useReducer(reducer, initialArg, init?)

在组件的顶层作用域调用 useReducer 以创建一个用于管理状态的 reducer。

import { useReducer } from 'react';

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

function MyComponent() {
  const [state, dispatch] = useReducer(reducer, { age: 42 });
  // ...

参数

  • reducer:用于更新 state 的纯函数。参数为 state 和 action,返回值是更新后的 state。state 与 action 可以是任意合法值。
  • initialArg:用于初始化 state 的任意值。初始值的计算逻辑取决于接下来的 init 参数。
  • 可选参数 init:用于计算初始值的函数。如果存在,使用 init(initialArg) 的执行结果作为初始值,否则使用 initialArg

返回值

useReducer 返回一个由两个值组成的数组:

  1. 当前的 state。初次渲染时,它是 init(initialArg)initialArg (如果没有 init 函数)。
  2. dispatch函数。用于更新 state 并触发组件的重新渲染。

dispatch 函数

dispatch 函数可以用来更新 state的值 并触发组件的重新渲染,它的用法其实和vue的store,react的状态管理库非常相似!

dispacth可以有很多,通过dispacth可以发送数据给reducer函数,函数内部,我们通过action可以拿到所有dispatch发送的数据,然后进行逻辑判断,更改state的值。

通常来说 action 是一个对象,其中 type 属性标识类型,其它属性携带额外信息。

代码解读

熟悉了它的语法后,我们的整合逻辑就非常好理解了。我们简化下逻辑:

import React, { useReducer, useState } from 'react';

// 定义 reducer 函数
const taskReducer = (state, action) => {
  switch (action.type) {
      // 根据不同逻辑,返回一个新的state的值
    default:
      return state;
  }
};

function TaskList() {
  const [tasks, dispatch] = useReducer(taskReducer, [
    { id: 1, title: '去贝加尔湖旅行', completed: false },
    { id: 2, title: '去烟台海边度假', completed: false },
    { id: 3, title: '再去一次厦门看海', completed: false },
  ]);
  // 通过dispatch发送数据给taskReducer
  return (
    <div>
      <h2>快乐就是哈哈哈的记事本</h2>
      <div key={task.id} style={{ marginTop: '10px' }}>
        <input
          type="checkbox"
          checked={task.completed}
          onChange={() => dispatch({ type: 'TOGGLE_TASK', payload: task.id })}
        />
        <button onClick={() => dispatch({ type: 'DELETE_TASK', payload: task.id })} style={{ marginLeft: '10px' }}>删除</button>
      </div>
    </div>
  );
}

export default TaskList;

useReducer的性能优化

我们先看看下面的代码

function createInitialState(username) {
  // ...
  // 生成初始值的一些逻辑
}

function TodoList({ username }) {
  const [state, dispatch] = useReducer(reducer, createInitialState(username));
  // ...

createInitialState方法用于生成初始值,但是在每一次渲染的时候都会被调用,如果创建了比较大的数组或计算是比较浪费性能的!

我们可以通过给 useReducer 的第三个参数传入 初始化函数 来解决这个问题:

function createInitialState(username) {
  // ...
}

function TodoList({ username }) {
  const [state, dispatch] = useReducer(reducer, username, createInitialState);
  // ...

需要注意的是你传入的参数是 createInitialState 这个 函数自身,而不是执行 createInitialState() 后的返回值

如果createInitialState可以直接计算出初始值,不需要默认的username,上面的代码可以进一步优化

function createInitialState() {
  // ...
}

function TodoList() {
  const [state, dispatch] = useReducer(reducer, null, createInitialState);
  // ...