likes
comments
collection
share

React: 如何使用useReducer

作者站长头像
站长
· 阅读数 55

React 中的reducer

随着Redux 作为 React 的状态管理解决方案的兴起,Reducer 的概念在 JavaScript 中变得流行。不过不用担心,你不需要学习 Redux 也能理解 Reducer。基本上,reducer 是用来管理应用程序中的状态的。例如,如果用户在 HTML 输入字段中写入内容,则应用程序必须管理此 UI 状态(例如受控组件)。

让我们深入了解实现细节:本质上,reducer 是一个函数,它接受两个参数——当前状态和一个动作——并基于两个参数返回一个新状态。在伪函数中可以表示为:

(state, action) => newState

例如,在 JavaScript 中将数字加一的场景如下所示:

function counterReducer(state, action) {
  return state + 1;
}

或者定义为 JavaScript 箭头函数,对于相同的逻辑,它看起来像以下方式:

const counterReducer = (state, action) => {
  return state + 1;
};

在这种情况下,当前状态是一个整数(例如计数),reducer 函数将计数加一。如果我们将参数重命名statecount,对于这个概念的新手来说,它可能更易读、更容易理解。但是,请记住,count仍然是状态:

const counterReducer = (count, action) => {
  return count + 1;
};

reducer 函数是一个没有任何副作用的纯函数,这意味着给定相同的输入(例如stateaction),预期输出(例如newState)将始终相同。这使得 reducer 函数非常适合推理状态变化并单独测试它们。您可以使用与参数相同的输入重复相同的测试,并始终期望相同的输出:

expect(counterReducer(0)).to.equal(1); // successful test
expect(counterReducer(0)).to.equal(1); // successful test

这就是 reducer 函数的本质。但是,我们还没有触及 reducer 的第二个参数:动作。action通常定义为具有type属性的对象。根据动作的类型,reducer 可以执行条件状态转换:

const counterReducer = (count, action) => {
  if (action.type === 'INCREASE') {
    return count + 1;
  }
  if (action.type === 'DECREASE') {
    return count - 1;
  }
  return count;
};

如果动作type不匹配任何条件,我们返回未更改的状态。测试具有多个状态转换的 reducer 函数——给定相同的输入,它总是会返回相同的预期输出——仍然如前所述保持正确,这在以下测试用例中得到了证明:

// successful tests
// because given the same input we can always expect the same output
expect(counterReducer(0, { type: 'INCREASE' })).to.equal(1);
expect(counterReducer(0, { type: 'INCREASE' })).to.equal(1);

// other state transition
expect(counterReducer(0, { type: 'DECREASE' })).to.equal(-1);

// if an unmatching action type is defined the current state is returned
expect(counterReducer(0, { type: 'UNMATCHING_ACTION' })).to.equal(0);

但是,您更有可能会看到 switch case 语句支持 if else 语句,以便为 reducer 函数映射多个状态转换。以下 reducer 执行与之前相同的逻辑,但使用 switch case 语句表示:

const counterReducer = (count, action) => {
  switch (action.type) {
    case 'INCREASE':
      return count + 1;
    case 'DECREASE':
      return count - 1;
    default:
      return count;
  }
};

在这种情况下,它count本身就是我们通过增加或减少计数来应用我们的状态更改的状态。然而,通常你不会有一个 JavaScript 原语(例如整数表示计数)作为状态,而是一个复杂的 JavaScript 对象。例如,计数可能是我们state对象的一个属性:

const counterReducer = (state, action) => {
  switch (action.type) {
    case 'INCREASE':
      return { ...state, count: state.count + 1 };
    case 'DECREASE':
      return { ...state, count: state.count - 1 };
    default:
      return state;
  }
};

如果您不立即了解此处代码中发生的情况,请不要担心。首先,一般来说有两件重要的事情需要理解:

  • reducer 函数处理的状态是不可变的。 这意味着传入的状态——作为参数传入——永远不会直接改变。因此,reducer 函数总是必须返回一个新的状态对象。
  • 由于我们知道状态是一个不可变的数据结构,我们可以使用JavaScript 扩展运算符从传入的状态和我们想要更改的部分(例如count属性)创建一个新的状态对象。这样,我们确保传入状态对象未触及的其他属性对于新状态对象仍然保持不变。

让我们通过另一个示例来看看代码中的这两个重要点,我们希望使用以下 reducer 函数更改人员对象的姓氏:

const personReducer = (person, action) => {
  switch (action.type) {
    case 'INCREASE_AGE':
      return { ...person, age: person.age + 1 };
    case 'CHANGE_LASTNAME':
      return { ...person, lastname: action.lastname };
    default:
      return person;
  }
};

我们可以在测试环境中通过以下方式更改用户的姓氏:

const initialState = {
  firstname: 'Liesa',
  lastname: 'Huppertz',
  age: 30,
};

const action = {
  type: 'CHANGE_LASTNAME',
  lastname: 'Wieruch',
};

const result = personReducer(initialState, action);

expect(result).to.equal({
  firstname: 'Liesa',
  lastname: 'Wieruch',
  age: 30,
});

您已经看到,通过在我们的 reducer 函数中使用 JavaScript 扩展运算符,我们将当前状态对象中的所有属性用于新状态对象,但覆盖lastname此新对象的特定属性(例如 )。这就是为什么您经常会看到扩展运算符用于保持状态操作不可变(= 状态不直接更改)。

您还看到了reducer 函数的另一个方面:为reducer 函数提供的动作可以在强制动作类型属性旁边有一个可选的有效负载(例如)。lastname有效载荷是执行状态转换的附加信息。例如,在我们的示例中,如果没有额外信息,reducer 不会知道我们人的新姓氏。

通常,动作的可选负载被放入另一个通用payload属性中,以保持动作对象的顶级属性更通用(例如{ type, payload })。这对于让类型和有效负载始终并排分离很有用。对于我们之前的代码示例,它会将操作更改为以下内容:

const action = {
  type: 'CHANGE_LASTNAME',
  payload: {
    lastname: 'Wieruch',
  },
};

reducer 函数也必须改变,因为它必须更深入地执行操作:

const personReducer = (person, action) => {
  switch (action.type) {
    case 'INCREASE_AGE':
      return { ...person, age: person.age + 1 };
    case 'CHANGE_LASTNAME':
      return { ...person, lastname: action.payload.lastname };
    default:
      return person;
  }
};

总结:

  • 语法: 本质上,reducer 函数表示为(state, action) => newState.

  • 不变性: 状态永远不会直接改变。相反,reducer 总是创建一个新状态。

  • 状态转换:  reducer 可以有条件状态转换。

  • 动作: 一个常见的动作对象带有一个强制类型属性和一个可选的有效负载:

    • type 属性选择条件状态转换。
    • 动作有效载荷提供状态转换的信息。

React 中的useReducer hook

useReducer 钩子用于复杂的状态和状态转换。它接受一个 reducer 函数和一个初始状态作为输入,并返回当前状态和一个调度函数作为输出,并使用数组解构

const initialTodos = [
  {
    id: 'a',
    task: 'Learn React',
    complete: false,
  },
  {
    id: 'b',
    task: 'Learn Firebase',
    complete: false,
  },
];

const todoReducer = (state, action) => {
  switch (action.type) {
    case 'DO_TODO':
      return state.map(todo => {
        if (todo.id === action.id) {
          return { ...todo, complete: true };
        } else {
          return todo;
        }
      });
    case 'UNDO_TODO':
      return state.map(todo => {
        if (todo.id === action.id) {
          return { ...todo, complete: false };
        } else {
          return todo;
        }
      });
    default:
      return state;
  }
};

const [todos, dispatch] = useReducer(todoReducer, initialTodos);

dispatch 函数可用于向 reducer 发送一个动作,该动作将隐式更改当前状态:

const [todos, dispatch] = React.useReducer(todoReducer, initialTodos);
dispatch({ type: 'DO_TODO', id: 'a' });

如果不在 React 组件中执行,前面的示例将无法工作,但它演示了如何通过调度操作来更改状态。让我们看看它在 React 组件中的样子。我们将从渲染项目列表的 React 组件开始。每个项目都有一个复选框作为受控组件。

import React from 'react';

const initialTodos = [
  {
    id: 'a',
    task: 'Learn React',
    complete: false,
  },
  {
    id: 'b',
    task: 'Learn Firebase',
    complete: false,
  },
];

const App = () => {
  const handleChange = () => {};
  return (
    <ul>
      {initialTodos.map(todo => (
        <li key={todo.id}>
          <label>
            <input
              type="checkbox"
              checked={todo.complete}
              onChange={handleChange}
            />
            {todo.task}
          </label>
        </li>
      ))}
    </ul>
  );
};

export default App;

尚无法使用处理函数更改项目的状态。然而,在我们这样做之前,我们需要通过使用它们作为我们的 useReducer 钩子的初始状态来使项目列表有状态,并使用之前定义的 reducer 函数:

import React from 'react';

const initialTodos = [...];

const todoReducer = (state, action) => {
  switch (action.type) {
    case 'DO_TODO':
      return state.map(todo => {
        if (todo.id === action.id) {
          return { ...todo, complete: true };
        } else {
          return todo;
        }
      });
    case 'UNDO_TODO':
      return state.map(todo => {
        if (todo.id === action.id) {
          return { ...todo, complete: false };
        } else {
          return todo;
        }
      });
    default:
      return state;
  }
};

const App = () => {
  const [todos, dispatch] = React.useReducer(
    todoReducer,
    initialTodos
  );

  const handleChange = () => {};

  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>
          ...
        </li>
      ))}
    </ul>
  );
};

export default App;

现在我们可以使用处理程序为我们的 reducer 函数调度一个动作。由于我们需要id作为 Todo 项的标识符来切换其complete标志,因此我们可以使用封装箭头函数在处理函数中传递该项:

const App = () => {
  const [todos, dispatch] = React.useReducer(
    todoReducer,
    initialTodos
  );

  const handleChange = todo => {
    dispatch({ type: 'DO_TODO', id: todo.id });
  };

  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>
          <label>
            <input
              type="checkbox"
              checked={todo.complete}
              onChange={() => handleChange(todo)}
            />
            {todo.task}
          </label>
        </li>
      ))}
    </ul>
  );
};

虽然这个实现只有一种方式:Todo 项目可以完成,但操作不能通过使用我们的 reducer 的第二个状态转换来反转。让我们通过检查 Todo 项是否完成来在我们的处理程序中实现此行为:

const App = () => {
  const [todos, dispatch] = React.useReducer(
    todoReducer,
    initialTodos
  );

  const handleChange = todo => {
    dispatch({
      type: todo.complete ? 'UNDO_TODO' : 'DO_TODO',
      id: todo.id,
    });
  };

  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>
          <label>
            <input
              type="checkbox"
              checked={todo.complete}
              onChange={() => handleChange(todo)}
            />
            {todo.task}
          </label>
        </li>
      ))}
    </ul>
  );
};
转载自:https://juejin.cn/post/7089069322944905229
评论
请登录