react之reducers

第三章 - 状态管理

迁移状态逻辑至 Reducer 中

对于拥有许多状态更新逻辑的组件来说,过于分散的事件处理程序可能会令人不知所措。对于这种情况,你可以将组件的所有状态更新逻辑整合到一个外部函数中,这个函数叫做reducer。

使用reducer 整合状态逻辑

随着组件复杂度的增加,你将很难一眼看清所有的组件状态更新逻辑。例如,下面的 TaskApp 组件有一个数组类型的状态 tasks,并通过三个不同的事件处理程序来实现任务的添加、删除和修改:

import { useState } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';

export default function TaskApp() {
  const [tasks, setTasks] = useState(initialTasks);

  function handleAddTask(text) {
    setTasks([
      ...tasks,
      {
        id: nextId++,
        text: text,
        done: false,
      },
    ]);
  }

  function handleChangeTask(task) {
    setTasks(
      tasks.map((t) => {
        if (t.id === task.id) {
          return task;
        } else {
          return t;
        }
      })
    );
  }

  function handleDeleteTask(taskId) {
    setTasks(tasks.filter((t) => t.id !== taskId));
  }

  return (
    <>
      <h1>布拉格的行程安排</h1>
      <AddTask onAddTask={handleAddTask} />
      <TaskList
        tasks={tasks}
        onChangeTask={handleChangeTask}
        onDeleteTask={handleDeleteTask}
      />
    </>
  );
}

let nextId = 3;
const initialTasks = [
  {id: 0, text: '参观卡夫卡博物馆', done: true},
  {id: 1, text: '看木偶戏', done: false},
  {id: 2, text: '打卡列侬墙', done: false},
];

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

Reducer 是处理状态的另一种方式。你可以通过三个步骤将 useState 迁移到 useReducer

  1. 将设置状态的逻辑修改成 dispatch 的一个action
  2. 编写一个reducer函数
  3. 在你的组件中使用reducer
第一步:将设置状态的逻辑修改成dispatch的一个action

移除所有的状态设置逻辑。只留下三个事件处理函数:

  • handleAddTask(text) 在用户点击 “添加” 时被调用。
  • handleChangeTask(task) 在用户切换任务或点击 “保存” 时被调用。
  • handleDeleteTask(taskId) 在用户点击 “删除” 时被调用。

使用reducer 管理状态与直接设置状态略有不同。他不是通过设置状态来告诉 React 要做什么,而是通过事件处理程序 dispatch一个"action" 来指明 “用户刚刚做了什么”。(而状态更新逻辑则保存在其他地方 !)因此,我们不再通过事件处理器直接"设置task",而是dispatch一个 "添加/修改/删除任务"的action。这更加符合用户的思维。

function handleAddTask(text) {
  dispatch({
    type: 'added',
    id: nextId++,
    text: text,
  });
}

function handleChangeTask(task) {
  dispatch({
    type: 'changed',
    task: task,
  });
}

function handleDeleteTask(taskId) {
  dispatch({
    type: 'deleted',
    id: taskId,
  });
}

你传递给 dispatch 的对象叫做 “action”

function handleDeleteTask(taskId) {
  dispatch(
    // "action" 对象:
    {
      type: 'deleted',
      id: taskId,
    }
  );
}

他就是一个普通的JavaScript对象。它的结构由你来决定,但通常来说,他应该至少包含可以表明发生了什么事情的信息。(在后面的步骤中,你将会学习如何添加一个 dispatch 函数。)

注意

action 对象可以有多种结构。

按照惯例,我们通常会添加一个字符串类型的 type 字段来描述发生了什么,并通过其它字段传递额外的信息。type 是特定于组件的,在这个例子中 addedaddded_task 都可以。选一个能描述清楚发生的事件的名字!

dispatch({

  // 针对特定的组件

  type: 'what_happened',

  // 其它字段放这里

});
第二步:编写一个reducer 函数

reducer 函数就是你放置状态逻辑的地方。他接受两个参数,分别为当前的state和action对象,并且返回的是更新后的state:

function yourReducer(state, action) {
  // 给 React 返回更新后的状态
}

React 会将状态设置为你从reducer返回的状态。

在这个例子中,要将状态设置逻辑从事件处理程序移动到reducer函数中,你需要:

  1. 声明当前状态(tasks) 作为第一个参数
  2. 声明action对象作为第二个参数
  3. 从reducer返回下一个状态 (react会将旧的状态设置为这个最新的状态)。

下面是所有迁移到reducer 函数的状态设置逻辑:

function tasksReducer (tasks,action) {
    if(action.type === 'added') {
		return [
            ...tasks,
            {
                id: action.id,
                text: action.text,
                done: false
            }
        ];
    }else if (action.type === 'changed') {
        return tasks.map((t) => {
            if(t.id === action.id) {
                return action.task;
            }else{
                return t;
            }
        })
    }else if (action.type === 'deleted') {
        return tasks.filter((t) => t.id !== action.id)
    }else {
        throw Error ('未知 action:' + action.type)
    }
}

由于reducer函数接受state (tasks) 作为参数,因此你可以在组件之外声明它。这减少了代码的缩进级别,提升了代码的可读性。(即可以将庞大的reducer函数单独提取成一个文件,与组件分开,提升代码阅读性)

上面的代码使用了 if/else 语句,但是在 reducers 中使用 switch 语句 是一种惯例。两种方式结果是相同的,但 switch 语句读起来一目了然。我们建议将每个 case 块包装到 {} 花括号中,这样在不同 case 中声明的变量就不会互相冲突。此外,case 通常应该以 return 结尾。如果你忘了 return,代码就会 进入 到下一个 case,这就会导致错误

第三步:在组件中使用reducer

最后,你需要将 tasksReducer导入到组件中。记得先从react中导入 useReducer Hook:

import { useReducer } from 'react';

接下来你就能替换掉之前的useState:

const [tasks,dispatch] = useReducer(tasksReducer, initialTasks);

useReducer 和 useState很相似 – 你必须传递给他一个初始状态,他会返回一个有状态的值和一个设置该状态的函数(在这个例子中就是dispatch函数)。但是,他们两个之间还是有差异的。

  • useReducer 钩子接受2个参数

    1. 一个初始的reducer函数
    2. 一个初始的state
  • 它返回如下内容:

    1. 一个有状态的值
    2. 一个dispatch函数 (用来派发用户操作给reducer)

现在一切都准备就绪了!我们在这里把 reducer 定义在了组件的末尾:

import { useReducer } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';

export default function TaskApp() {
  const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

  function handleAddTask(text) {
    dispatch({
      type: 'added',
      id: nextId++,
      text: text,
    });
  }

  function handleChangeTask(task) {
    dispatch({
      type: 'changed',
      task: task,
    });
  }

  function handleDeleteTask(taskId) {
    dispatch({
      type: 'deleted',
      id: taskId,
    });
  }

  return (
    <>
      <h1>布拉格的行程安排</h1>
      <AddTask onAddTask={handleAddTask} />
      <TaskList
        tasks={tasks}
        onChangeTask={handleChangeTask}
        onDeleteTask={handleDeleteTask}
      />
    </>
  );
}

function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added': {
      return [
        ...tasks,
        {
          id: action.id,
          text: action.text,
          done: false,
        },
      ];
    }
    case 'changed': {
      return tasks.map((t) => {
        if (t.id === action.task.id) {
          return action.task;
        } else {
          return t;
        }
      });
    }
    case 'deleted': {
      return tasks.filter((t) => t.id !== action.id);
    }
    default: {
      throw Error('未知 action: ' + action.type);
    }
  }
}

let nextId = 3;
const initialTasks = [
  {id: 0, text: '参观卡夫卡博物馆', done: true},
  {id: 1, text: '看木偶戏', done: false},
  {id: 2, text: '打卡列侬墙', done: false}
];

如果有需要,你甚至可以把reducer移到一个单独的文件中。当像这样分离关注点时,我们可以更容易地理解组件逻辑。现在,事件处理程序只通过派发 action 来指定 发生了什么,而 reducer 函数通过响应 actions 来决定 状态如何更新

对比 useState 和 useReducer

Reducer并非没有缺点,以下是比较它们的几种方法:

  • 代码体积: 通常,在使用 useState 时,一开始只需要编写少量代码。而 useReducer 必须提前编写 reducer 函数和需要调度的 actions。但是,当多个事件处理程序以相似的方式修改 state 时,useReducer 可以减少代码量。
  • 可读性: 当状态更新逻辑足够简单时,useState 的可读性还行。但是,一旦逻辑变得复杂起来,它们会使组件变得臃肿且难以阅读。在这种情况下,useReducer 允许你将状态更新逻辑与事件处理程序分离开来。
  • 可调试性: 当使用 useState 出现问题时, 你很难发现具体原因以及为什么。 而使用 useReducer 时, 你可以在 reducer 函数中通过打印日志的方式来观察每个状态的更新,以及为什么要更新(来自哪个 action)。 如果所有 action 都没问题,你就知道问题出在了 reducer 本身的逻辑中。 然而,与使用 useState 相比,你必须单步执行更多的代码。
  • 可测试性: reducer 是一个不依赖于组件的纯函数。这就意味着你可以单独对它进行测试。一般来说,我们最好是在真实环境中测试组件,但对于复杂的状态更新逻辑,针对特定的初始状态和 action,断言 reducer 返回的特定状态会很有帮助。
  • 个人偏好: 并不是所有人都喜欢用 reducer,没关系,这是个人偏好问题。你可以随时在 useStateuseReducer 之间切换,它们能做的事情是一样的!

如果你在修改某些组件状态时经常出现问题或者想给组件添加更多逻辑时,我们建议你还是使用 reducer。当然,你也不必整个项目都用 reducer,这是可以自由搭配的。你甚至可以在一个组件中同时使用 useStateuseReducer

编写一个好的reducers

编写 reducers 时最好牢记以下两点:

  • reducers必须是纯粹的。 这一点和状态更新函数是相似的,reducer是在渲染时运行的!(action会排队直到下一次渲染)。这就意味这reducers必须纯净,即当输入相同时,输出也要相同。他们不应该包含异步请求,定时器或者任何副作用 (对组件外部有影响的操作)。他们应该以不可变值的方式去更新对象和数组。
  • 每个action都描述了一个单一的用户交互,即使它会引发数据的多个变化。举个例子,如果用户在一个有reducer管理的表单 (包含五个表单项) 中点击了重置按钮,那么dispatch一个 reset_form 的action比dispatch五个单独的set_field 的action更加合理。如果你在一个reducer中打印了所有的action日志,那么这个日志应该是清晰的,他能让你以某种步骤复现已发生的交互或响应,这对代码调试很有帮助。

使用 Immer 简化 reducers

与在平常的 state 中 修改对象数组 一样,你可以使用 Immer 这个库来简化 reducer。在这里,useImmerReducer 让你可以通过 pusharr[i] = 来修改 state :

import { useImmerReducer } from 'use-immer';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';

function tasksReducer(draft, action) {
  switch (action.type) {
    case 'added': {
      draft.push({
        id: action.id,
        text: action.text,
        done: false,
      });
      break;
    }
    case 'changed': {
      const index = draft.findIndex((t) => t.id === action.task.id);
      draft[index] = action.task;
      break;
    }
    case 'deleted': {
      return draft.filter((t) => t.id !== action.id);
    }
    default: {
      throw Error('未知 action:' + action.type);
    }
  }
}

export default function TaskApp() {
  const [tasks, dispatch] = useImmerReducer(tasksReducer, initialTasks);

  function handleAddTask(text) {
    dispatch({
      type: 'added',
      id: nextId++,
      text: text,
    });
  }

  function handleChangeTask(task) {
    dispatch({
      type: 'changed',
      task: task,
    });
  }

  function handleDeleteTask(taskId) {
    dispatch({
      type: 'deleted',
      id: taskId,
    });
  }

  return (
    <>
      <h1>布拉格的行程安排</h1>
      <AddTask onAddTask={handleAddTask} />
      <TaskList
        tasks={tasks}
        onChangeTask={handleChangeTask}
        onDeleteTask={handleDeleteTask}
      />
    </>
  );
}

let nextId = 3;
const initialTasks = [
  {id: 0, text: '参观卡夫卡博物馆', done: true},
  {id: 1, text: '看木偶戏', done: false},
  {id: 2, text: '打卡列侬墙', done: false},
];

Reducers应该是纯净的,所以它们不应该去修改state。而Immer为你提供了一种特殊的 draft 对象,你可以通过它安全的修改state。在底层,Immer 会基于当前state 创建一个副本。这就是为什么 useImmerReducer 来管理 reducers 时,可以直接修改第一个参数,且不需要返回一个新的state

摘要
  • 把 useState 转换成 useReducer:
    • 通过事件处理函数 dispatch actions;
    • 编写一个reducer函数,他接受传入的state和一个action,并返回一个新的state;
    • 使用useReducer 替换 useState
  • Reducers 可能需要你写更多的代码,但这有利于代码的调试和测试
  • Reducers必须是纯净的
  • 每个action 都描述了一个单一的用户交互
  • 使用 Immer 来帮助你在reducer里直接修改状态
  • 8
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
React Context 结合 Reducer 可以实现状态管理和状态更新的功能。Reducer 是一个纯函数,接收当前的状态和一个 action,返回一个新的状态。通过 Context 和 Reducer 的结合使用,可以将状态和状态更新逻辑分离到不同的文件中,使得组件的代码更加简洁和清晰。 下面是一个示例代码: ```jsx // 创建一个 context 对象 const MyContext = React.createContext(); // 定义 reducer 函数 function reducer(state, action) { switch (action.type) { case "increment": return { count: state.count + 1 }; case "decrement": return { count: state.count - 1 }; default: throw new Error(); } } // 父组件 function ParentComponent() { const [state, dispatch] = useReducer(reducer, { count: 0 }); // 将 reducer 的 dispatch 函数作为属性传递给子组件 return ( <MyContext.Provider value={{ state, dispatch }}> <ChildComponent /> </MyContext.Provider> ); } // 子组件 function ChildComponent() { // 使用 useContext 钩子获取父组件提供的 context 对象 const { state, dispatch } = useContext(MyContext); return ( <div> <p>当前的计数:{state.count}</p> <button onClick={() => dispatch({ type: "increment" })}>增加</button> <button onClick={() => dispatch({ type: "decrement" })}>减少</button> </div> ); } ``` 在上面的代码中,父组件中使用 `useReducer` 钩子创建了一个状态以及一个 dispatch 函数。将状态和 dispatch 函数通过 Context 提供给子组件。在子组件中,通过 `useContext` 钩子获取父组件提供的 state 和 dispatch 对象。当用户点击子组件中的增加或减少按钮时,会调用 dispatch 函数并传入对应的 action 对象,从而触发状态更新,更新后的状态会自动传递给所有使用了该 Context 的组件,实现了状态的管理和更新。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值