将状态逻辑提取到 Reducer 中

具有许多分布在许多事件处理程序中的状态更新的组件可能会让人不知所措。对于这些情况,你可以将组件外部的所有状态更新逻辑合并到一个称为 reducer 的函数中。

¥Components with many state updates spread across many event handlers can get overwhelming. For these cases, you can consolidate all the state update logic outside your component in a single function, called a reducer.

你将学习到

  • 什么是 reducer 函数

    ¥What a reducer function is

  • 如何重构 useStateuseReducer

    ¥How to refactor useState to useReducer

  • 什么时候使用 reducer

    ¥When to use a reducer

  • 如何编写好一个

    ¥How to write one well

使用 reducer 整合状态逻辑

¥Consolidate state logic with a reducer

随着你的组件变得越来越复杂,一目了然地了解组件状态更新的所有不同方式会变得越来越困难。例如,下面的 TaskApp 组件包含状态中的 tasks 数组,并使用三个不同的事件处理程序来添加、删除和编辑任务:

¥As your components grow in complexity, it can get harder to see at a glance all the different ways in which a component’s state gets updated. For example, the TaskApp component below holds an array of tasks in state and uses three different event handlers to add, remove, and edit 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>Prague itinerary</h1>
      <AddTask onAddTask={handleAddTask} />
      <TaskList
        tasks={tasks}
        onChangeTask={handleChangeTask}
        onDeleteTask={handleDeleteTask}
      />
    </>
  );
}

let nextId = 3;
const initialTasks = [
  {id: 0, text: 'Visit Kafka Museum', done: true},
  {id: 1, text: 'Watch a puppet show', done: false},
  {id: 2, text: 'Lennon Wall pic', done: false},
];

它的每个事件处理程序都调用 setTasks 以更新状态。随着这个组件的增长,散布在其中的状态逻辑的数量也会增加。为了降低这种复杂性并将所有逻辑保存在一个易于访问的位置,你可以将该状态逻辑移至组件外部的单个函数中,称为 “reducer”。

¥Each of its event handlers calls setTasks in order to update the state. As this component grows, so does the amount of state logic sprinkled throughout it. To reduce this complexity and keep all your logic in one easy-to-access place, you can move that state logic into a single function outside your component, called a “reducer”.

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

¥Reducers are a different way to handle state. You can migrate from useState to useReducer in three steps:

  1. 从设置状态转移到调度动作。

    ¥Move from setting state to dispatching actions.

  2. 编写一个 reducer 函数。

    ¥Write a reducer function.

  3. 使用组件中的 reducer。

    ¥Use the reducer from your component.

步骤 1:从设置状态转移到调度动作

¥Step 1: Move from setting state to dispatching actions

你的事件处理程序当前通过设置状态指定要执行的操作:

¥Your event handlers currently specify what to do by setting state:

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));
}

删除所有状态设置逻辑。你剩下的是三个事件处理程序:

¥Remove all the state setting logic. What you are left with are three event handlers:

  • 当用户按下 “添加” 时调用 handleAddTask(text)

    ¥handleAddTask(text) is called when the user presses “Add”.

  • 当用户切换任务或按下 “保存” 时调用 handleChangeTask(task)

    ¥handleChangeTask(task) is called when the user toggles a task or presses “Save”.

  • 当用户按下 “删除” 时调用 handleDeleteTask(taskId)

    ¥handleDeleteTask(taskId) is called when the user presses “Delete”.

使用 reducer 管理状态与直接设置状态略有不同。不是通过设置状态来告诉 React “做什么”,而是通过从事件处理程序中分派 “操作” 来指定 “用户刚刚做了什么”。(状态更新逻辑将位于其他地方!)因此,你不是通过事件处理程序“设置 tasks”,而是调度 “添加/更改/删除任务” 操作。这更能描述用户的意图。

¥Managing state with reducers is slightly different from directly setting state. Instead of telling React “what to do” by setting state, you specify “what the user just did” by dispatching “actions” from your event handlers. (The state update logic will live elsewhere!) So instead of “setting tasks” via an event handler, you’re dispatching an “added/changed/deleted a task” action. This is more descriptive of the user’s intent.

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 的对象称为 “操作”:

¥The object you pass to dispatch is called an “action”:

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

它是一个常规的 JavaScript 对象。你决定在其中放入什么,但通常它应该包含有关发生的事情的最少信息。(你将在后面的步骤中添加 dispatch 函数本身。)

¥It is a regular JavaScript object. You decide what to put in it, but generally it should contain the minimal information about what happened. (You will add the dispatch function itself in a later step.)

注意

一个操作对象可以有任何形状。

¥An action object can have any shape.

按照惯例,通常给它一个字符串 type 来描述发生的事情,并在其他字段中传递任何附加信息。type 特定于组件,因此在此示例中,'added''added_task' 都可以。选择一个能说明发生了什么的名字!

¥By convention, it is common to give it a string type that describes what happened, and pass any additional information in other fields. The type is specific to a component, so in this example either 'added' or 'added_task' would be fine. Choose a name that says what happened!

dispatch({
// specific to component
type: 'what_happened',
// other fields go here
});

步骤 2:编写一个 reducer 函数

¥Step 2: Write a reducer function

reducer 函数是放置状态逻辑的地方。它有两个参数,当前状态和操作对象,并返回下一个状态:

¥A reducer function is where you will put your state logic. It takes two arguments, the current state and the action object, and it returns the next state:

function yourReducer(state, action) {
// return next state for React to set
}

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

¥React will set the state to what you return from the reducer.

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

¥To move your state setting logic from your event handlers to a reducer function in this example, you will:

  1. 将当前状态 (tasks) 声明为第一个参数。

    ¥Declare the current state (tasks) as the first argument.

  2. action 对象声明为第二个参数。

    ¥Declare the action object as the second argument.

  3. 从 reducer 返回下一个状态(React 将状态设置为)。

    ¥Return the next state from the reducer (which React will set the state to).

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

¥Here is all the state setting logic migrated to a reducer function:

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.task.id) {
return action.task;
} else {
return t;
}
});
} else if (action.type === 'deleted') {
return tasks.filter((t) => t.id !== action.id);
} else {
throw Error('Unknown action: ' + action.type);
}
}

由于 reducer 函数将状态(tasks) 作为参数,因此你可以在组件外部声明它。这会减少缩进级别,并使代码更易于阅读。

¥Because the reducer function takes state (tasks) as an argument, you can declare it outside of your component. This decreases the indentation level and can make your code easier to read.

注意

上面的代码使用了 if/else 语句,但在 reducer 中使用 switch 语句 是一种惯例。结果是一样的,但是一目了然地阅读 switch 语句会更容易。

¥The code above uses if/else statements, but it’s a convention to use switch statements inside reducers. The result is the same, but it can be easier to read switch statements at a glance.

我们将在本文档的其余部分使用它们,如下所示:

¥We’ll be using them throughout the rest of this documentation like so:

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('Unknown action: ' + action.type);
}
}
}

我们建议将每个 case 块封装到 {} 大括号中,以便在不同 case 内声明的变量不会相互冲突。此外,case 通常应以 return 结尾。如果你忘记了 return,代码会把 “落空” 变成下一个 case,这会导致错误!

¥We recommend wrapping each case block into the { and } curly braces so that variables declared inside of different cases don’t clash with each other. Also, a case should usually end with a return. If you forget to return, the code will “fall through” to the next case, which can lead to mistakes!

如果你还不习惯 switch 语句,那么使用 if/else 完全没问题。

¥If you’re not yet comfortable with switch statements, using if/else is completely fine.

深入研究

为什么 reducer 这么调用?

¥Why are reducers called this way?

尽管 reducer 可以 “reduce” 组件内的代码量,但它们实际上是根据你可以对数组执行的 reduce() 操作命名的。

¥Although reducers can “reduce” the amount of code inside your component, they are actually named after the reduce() operation that you can perform on arrays.

reduce() 操作允许你从多个数组中获取单个值,而 “累加” 操作允许你从多个值中获取单个值:

¥The reduce() operation lets you take an array and “accumulate” a single value out of many:

const arr = [1, 2, 3, 4, 5];
const sum = arr.reduce(
(result, number) => result + number
); // 1 + 2 + 3 + 4 + 5

你传递给 reduce 的函数称为 “reducer”。它获取到目前为止的结果和当前项目,然后返回下一个结果。React reducer 是同一想法的一个例子:他们获取到目前为止的状态和操作,并返回下一个状态。通过这种方式,它们随着时间的推移将动作累积成状态。

¥The function you pass to reduce is known as a “reducer”. It takes the result so far and the current item, then it returns the next result. React reducers are an example of the same idea: they take the state so far and the action, and return the next state. In this way, they accumulate actions over time into state.

你甚至可以将 reduce() 方法与 initialStateactions 数组一起使用,通过将 reducer 函数传递给它来计算最终状态:

¥You could even use the reduce() method with an initialState and an array of actions to calculate the final state by passing your reducer function to it:

import tasksReducer from './tasksReducer.js';

let initialState = [];
let actions = [
  {type: 'added', id: 1, text: 'Visit Kafka Museum'},
  {type: 'added', id: 2, text: 'Watch a puppet show'},
  {type: 'deleted', id: 1},
  {type: 'added', id: 3, text: 'Lennon Wall pic'},
];

let finalState = actions.reduce(tasksReducer, initialState);

const output = document.getElementById('output');
output.textContent = JSON.stringify(finalState, null, 2);

你可能不需要自己执行此操作,但这与 React 所做的类似!

¥You probably won’t need to do this yourself, but this is similar to what React does!

步骤 3:使用组件中的 reducer

¥Step 3: Use the reducer from your component

最后,你需要将 tasksReducer 连接到你的组件。从 React 导入 useReducer 钩子:

¥Finally, you need to hook up the tasksReducer to your component. Import the useReducer Hook from React:

import { useReducer } from 'react';

然后你可以替换 useState

¥Then you can replace useState:

const [tasks, setTasks] = useState(initialTasks);

像这样使用 useReducer

¥with useReducer like so:

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

useReducer 钩子与 useState 类似 - 你必须向它传递一个初始状态,它返回一个有状态值和设置状态的方法(在本例中为调度函数)。但这有点不同。

¥The useReducer Hook is similar to useState—you must pass it an initial state and it returns a stateful value and a way to set state (in this case, the dispatch function). But it’s a little different.

useReducer 钩子有两个参数:

¥The useReducer Hook takes two arguments:

  1. reducer 函数

    ¥A reducer function

  2. 初始状态

    ¥An initial state

它返回:

¥And it returns:

  1. 有状态的值

    ¥A stateful value

  2. 调度函数(“调度” 用户操作到 reducer)

    ¥A dispatch function (to “dispatch” user actions to the reducer)

现在它已经完全连接起来了!这里,reducer 在组件文件的底部声明:

¥Now it’s fully wired up! Here, the reducer is declared at the bottom of the component file:

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>Prague itinerary</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('Unknown action: ' + action.type);
    }
  }
}

let nextId = 3;
const initialTasks = [
  {id: 0, text: 'Visit Kafka Museum', done: true},
  {id: 1, text: 'Watch a puppet show', done: false},
  {id: 2, text: 'Lennon Wall pic', done: false},
];

如果你愿意,你甚至可以将 reducer 移动到不同的文件:

¥If you want, you can even move the reducer to a different file:

import { useReducer } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
import tasksReducer from './tasksReducer.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>Prague itinerary</h1>
      <AddTask onAddTask={handleAddTask} />
      <TaskList
        tasks={tasks}
        onChangeTask={handleChangeTask}
        onDeleteTask={handleDeleteTask}
      />
    </>
  );
}

let nextId = 3;
const initialTasks = [
  {id: 0, text: 'Visit Kafka Museum', done: true},
  {id: 1, text: 'Watch a puppet show', done: false},
  {id: 2, text: 'Lennon Wall pic', done: false},
];

当你像这样分离关注点时,组件逻辑可以更容易阅读。现在,事件处理程序仅通过调度操作指定发生了什么,reducer 函数确定状态如何更新以响应它们。

¥Component logic can be easier to read when you separate concerns like this. Now the event handlers only specify what happened by dispatching actions, and the reducer function determines how the state updates in response to them.

比较 useStateuseReducer

¥Comparing useState and useReducer

reducer 并非没有缺点!你可以通过以下几种方式比较它们:

¥Reducers are not without downsides! Here’s a few ways you can compare them:

  • 代码大小:通常,对于 useState,你必须预先编写更少的代码。对于 useReducer,你必须同时编写 reducer 函数和调度操作。但是,如果许多事件处理程序以类似方式修改状态,useReducer 可以帮助减少代码。

    ¥Code size: Generally, with useState you have to write less code upfront. With useReducer, you have to write both a reducer function and dispatch actions. However, useReducer can help cut down on the code if many event handlers modify state in a similar way.

  • 可读性当状态更新很简单时,useState 非常容易阅读。当它们变得更复杂时,它们会使你的组件代码膨胀并使其难以扫描。在这种情况下,useReducer 可以让你清楚地将更新逻辑的方式与事件处理程序发生的事情分开。

    ¥Readability: useState is very easy to read when the state updates are simple. When they get more complex, they can bloat your component’s code and make it difficult to scan. In this case, useReducer lets you cleanly separate the how of update logic from the what happened of event handlers.

  • 调试当你遇到 useState 的错误时,可能很难判断状态设置错误的位置以及原因。使用 useReducer,你可以在 reducer 中添加一个控制台日志,以查看每个状态更新,以及它发生的原因(由于哪个 action)。如果每个 action 都是正确的,你就会知道错误出在 reducer 逻辑本身。但是,与 useState 相比,你必须逐步执行更多代码。

    ¥Debugging: When you have a bug with useState, it can be difficult to tell where the state was set incorrectly, and why. With useReducer, you can add a console log into your reducer to see every state update, and why it happened (due to which action). If each action is correct, you’ll know that the mistake is in the reducer logic itself. However, you have to step through more code than with useState.

  • 测试reducer 是一个不依赖于你的组件的纯函数。这意味着你可以隔离地单独导出和测试它。虽然通常最好在更真实的环境中测试组件,但对于复杂的状态更新逻辑,断言你的 reducer 为特定的初始状态和操作返回特定的状态可能很有用。

    ¥Testing: A reducer is a pure function that doesn’t depend on your component. This means that you can export and test it separately in isolation. While generally it’s best to test components in a more realistic environment, for complex state update logic it can be useful to assert that your reducer returns a particular state for a particular initial state and action.

  • 个人喜好:有些人喜欢 reducer,有些人不喜欢。没关系。这是一个偏好问题。你始终可以在 useStateuseReducer 之间来回转换:它们是等价的!

    ¥Personal preference: Some people like reducers, others don’t. That’s okay. It’s a matter of preference. You can always convert between useState and useReducer back and forth: they are equivalent!

如果你经常遇到一些组件由于状态更新不正确而导致的错误,并且想在其代码中引入更多结构,我们建议使用 reducer。你不必为所有事情都使用 reducer:随意混搭!你甚至可以在同一个组件中使用 useStateuseReducer

¥We recommend using a reducer if you often encounter bugs due to incorrect state updates in some component, and want to introduce more structure to its code. You don’t have to use reducers for everything: feel free to mix and match! You can even useState and useReducer in the same component.

写好 reducer

¥Writing reducers well

编写 reducer 时请牢记这两个提示:

¥Keep these two tips in mind when writing reducers:

  • Reducer 必须是纯粹的。与 状态更新函数 类似,reducer 在渲染过程中运行!(操作会排队直到下一次渲染。)这意味着 reducer 必须是纯的 - 相同的输入总是产生相同的输出。它们不应该发送请求、安排超时或执行任何副作用(影响组件外部事物的操作)。它们应该无突变地更新 对象数组

    ¥Reducers must be pure. Similar to state updater functions, reducers run during rendering! (Actions are queued until the next render.) This means that reducers must be pure—same inputs always result in the same output. They should not send requests, schedule timeouts, or perform any side effects (operations that impact things outside the component). They should update objects and arrays without mutations.

  • 每个操作都描述了一次用户交互,即使这会导致数据发生多次更改。例如,如果用户在具有由 reducer 管理的五个字段的表单上按 “重置”,则分派一个 reset_form 操作比分派五个单独的 set_field 操作更有意义。如果你记录 reducer 中的每个操作,那么该日志应该足够清晰,以便你重建以何种顺序发生的交互或响应。这有助于调试!

    ¥Each action describes a single user interaction, even if that leads to multiple changes in the data. For example, if a user presses “Reset” on a form with five fields managed by a reducer, it makes more sense to dispatch one reset_form action rather than five separate set_field actions. If you log every action in a reducer, that log should be clear enough for you to reconstruct what interactions or responses happened in what order. This helps with debugging!

使用 Immer 编写简洁的 reducer

¥Writing concise reducers with Immer

就像 更新对象数组 处于常规状态一样,你可以使用 Immer 库使 reducer 更加简洁。在这里,useImmerReducer 允许你使用 pusharr[i] = 赋值来改变状态:

¥Just like with updating objects and arrays in regular state, you can use the Immer library to make reducers more concise. Here, useImmerReducer lets you mutate the state with push or arr[i] = assignment:

{
  "dependencies": {
    "immer": "1.7.3",
    "react": "latest",
    "react-dom": "latest",
    "react-scripts": "latest",
    "use-immer": "0.5.1"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  },
  "devDependencies": {}
}

Reducer 必须是纯粹的,所以它们不应该改变状态。但是 Immer 为你提供了一个可以安全修改的特殊 draft 对象。在引擎盖下,Immer 将使用你对 draft 所做的更改创建你的状态副本。这就是为什么由 useImmerReducer 管理的 reducer 可以改变它们的第一个参数并且不需要返回状态。

¥Reducers must be pure, so they shouldn’t mutate state. But Immer provides you with a special draft object which is safe to mutate. Under the hood, Immer will create a copy of your state with the changes you made to the draft. This is why reducers managed by useImmerReducer can mutate their first argument and don’t need to return state.

回顾

  • useState 转换为 useReducer

    ¥To convert from useState to useReducer:

    1. 从事件处理程序调度操作。

      ¥Dispatch actions from event handlers.

    2. 编写一个 reducer 函数,返回给定状态和操作的下一个状态。

      ¥Write a reducer function that returns the next state for a given state and action.

    3. useReducer 替换 useState

      ¥Replace useState with useReducer.

  • Reducer 需要你编写更多代码,但它们有助于调试和测试。

    ¥Reducers require you to write a bit more code, but they help with debugging and testing.

  • Reducer 必须是纯粹的。

    ¥Reducers must be pure.

  • 每个操作都描述了一次用户交互。

    ¥Each action describes a single user interaction.

  • 如果你想以可变风格编写 reducer,请使用 Immer。

    ¥Use Immer if you want to write reducers in a mutating style.

挑战 1 / 4:
从事件处理程序调度操作

¥Dispatch actions from event handlers

目前,ContactList.jsChat.js 中的事件处理程序有 // TODO 条评论。这就是为什么键入输入不起作用,并且单击按钮不会更改所选收件人的原因。

¥Currently, the event handlers in ContactList.js and Chat.js have // TODO comments. This is why typing into the input doesn’t work, and clicking on the buttons doesn’t change the selected recipient.

将这两个 // TODO 替换成 dispatch 相应动作的代码。要查看预期的形状和动作类型,请检查 messengerReducer.js 中的 reducer。reducer 已经写好了,所以你不需要改变它。你只需要派发 ContactList.jsChat.js 中的操作。

¥Replace these two // TODOs with the code to dispatch the corresponding actions. To see the expected shape and the type of the actions, check the reducer in messengerReducer.js. The reducer is already written so you won’t need to change it. You only need to dispatch the actions in ContactList.js and Chat.js.

import { useReducer } from 'react';
import Chat from './Chat.js';
import ContactList from './ContactList.js';
import { initialState, messengerReducer } from './messengerReducer';

export default function Messenger() {
  const [state, dispatch] = useReducer(messengerReducer, initialState);
  const message = state.message;
  const contact = contacts.find((c) => c.id === state.selectedId);
  return (
    <div>
      <ContactList
        contacts={contacts}
        selectedId={state.selectedId}
        dispatch={dispatch}
      />
      <Chat
        key={contact.id}
        message={message}
        contact={contact}
        dispatch={dispatch}
      />
    </div>
  );
}

const contacts = [
  {id: 0, name: 'Taylor', email: 'taylor@mail.com'},
  {id: 1, name: 'Alice', email: 'alice@mail.com'},
  {id: 2, name: 'Bob', email: 'bob@mail.com'},
];


React 中文网 - 粤ICP备13048890号