【译】在 React Hooks 中使用 useReducer 的几种用例

原文:How to use useReducer in React Hooks for performance optimization html

github 的地址 欢迎 star!
React Hook 出来已经有一段时间了,具体的一些用法以及它解决的痛点,能够查看 Dan 的两篇文章 useEffect 完整指南以及编写有弹性的组件进行详细了解。react

本文主要是介绍了6种在 React Hooks 使用 useReducer 的不一样的方法git

前言

React Hooks API正式在 React V16.8 版本发布了。这篇博客,主要是介绍了其中 useReducer 的各类用法示例。在读以前,你确保你已经看过 React Hooks官方指南github

useReducer hook 属于官方扩展的 hooks:redux

是 useState 的另外一种替代。它接受(state, action) => newState,而且返回了一个与当前state成对的dispatch的方法。(若是你熟悉 Redux ,你也很快就能理解它是怎么工做的。)react-native

尽管 useReducer 是扩展的 hook, 而 useState 是基本的 hook,但 useState 实际上执行的也是一个 useReducer。这意味着 useReducer 是更原生的,你能在任何使用 useState 的地方都替换成使用 useReducer。Reducer 如此给力,因此有各类各样的用例。浏览器

本文接下来就介绍了几种表明性的用例。每一个例子都表明一种特定的用例,都有相关的代码。性能优化

用例1:最小(简单)的模式

能够看这个简单示例的代码。下文都是用这个计数的例子作延伸的。bash

const initialState = 0;
const reducer = (state, action) => {
  switch (action) {
    case 'increment': return state + 1;
    case 'decrement': return state - 1;
    case 'reset': return 0;
    default: throw new Error('Unexpected action');
  }
};
复制代码

首先,咱们定义了初始化的 initialState 以及 reducer。注意这里的 state 仅是一个数字,不是对象。熟悉 Redux 的开发者多是困惑的,但在 hook 中是适宜的。此外,action 仅是一个普通的字符串。ide

下面是一个使用 useReducer 的组件。

const Example01 = () => {
  const [count, dispatch] = useReducer(reducer, initialState);
  return (
    <div>
      {count}
      <button onClick={() => dispatch('increment')}>+1</button>
      <button onClick={() => dispatch('decrement')}>-1</button>
      <button onClick={() => dispatch('reset')}>reset</button>
    </div>
  );
};
复制代码

当用户点击一个按钮,它就会 dispatch 一个 action 来更新计数值 count,页面就会展现更新以后 count。你能够在 reducer中尽量多定义 action,但这种模式有局限,它的 action是有限的。

下面是完整的代码:

import React, { useReducer } from 'react';

const initialState = 0;
const reducer = (state, action) => {
  switch (action) {
    case 'increment': return state + 1;
    case 'decrement': return state - 1;
    case 'reset': return 0;
    default: throw new Error('Unexpected action');
  }
};

const Example01 = () => {
  const [count, dispatch] = useReducer(reducer, initialState);
  return (
    <div>
      {count}
      <button onClick={() => dispatch('increment')}>+1</button>
      <button onClick={() => dispatch('decrement')}>-1</button>
      <button onClick={() => dispatch('reset')}>reset</button>
    </div>
  );
};

export default Example01;
复制代码

用例2:action是一个对象

这个例子 Redux的使用者是熟悉的。咱们使用了 state对象以及一个 action对象。

const initialState = {
  count1: 0,
  count2: 0,
};
const reducer = (state, action) => {
  switch (action.type) {
    case 'increment1':
      return { ...state, count1: state.count1 + 1 };
    case 'decrement1':
      return { ...state, count1: state.count1 - 1 };
    case 'set1':
      return { ...state, count1: action.count };
    case 'increment2':
      return { ...state, count2: state.count2 + 1 };
    case 'decrement2':
      return { ...state, count2: state.count2 - 1 };
    case 'set2':
      return { ...state, count2: action.count };
    default:
      throw new Error('Unexpected action');
  }
};
复制代码

在 state中存放了两个数字。咱们能使用复杂的对象表示 state,只要能把 reducer组织好(列如:react-redux中 combineReducers)。另外,由于 action是一个对象,除了 type值,你还能够给它添加其余属性像action.count。这个例子中 reducer 是有点杂乱的,但不妨碍咱们下面这样使用它:

const Example02 = () => {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      <div>
        {state.count1}
        <button onClick={() => dispatch({ type: 'increment1' })}>+1</button>
        <button onClick={() => dispatch({ type: 'decrement1' })}>-1</button>
        <button onClick={() => dispatch({ type: 'set1', count: 0 })}>reset</button>
      </div>
      <div>
        {state.count2}
        <button onClick={() => dispatch({ type: 'increment2' })}>+1</button>
        <button onClick={() => dispatch({ type: 'decrement2' })}>-1</button>
        <button onClick={() => dispatch({ type: 'set2', count: 0 })}>reset</button>
      </div>
    </>
  );
};
复制代码

注意到 state 中有两个计数器,定义各自相应的 action 类型来更新它们。在线示例点击这里

用例3:使用多个useReducer

上面的单个 state 中出现两个计数器,这就是一种典型的全局 state 的方法。但咱们仅仅须要使用本地(局部)的 state,故有另一种方法,可使用 useReducer 两次。

const initialState = 0;
const reducer = (state, action) => {
  switch (action.type) {
    case 'increment': return state + 1;
    case 'decrement': return state - 1;
    case 'set': return action.count;
    default: throw new Error('Unexpected action');
  }
};
复制代码

这里的 state 是一个数字,而不是对象,它和用例1中是一致的。注意不一样点这里的 action 是一个对象。

组件如何使用呢

const Example03 = () => {
  const [count1, dispatch1] = useReducer(reducer, initialState);
  const [count2, dispatch2] = useReducer(reducer, initialState);
  return (
    <>
      <div>
        {count1}
        <button onClick={() => dispatch1({ type: 'increment' })}>+1</button>
        <button onClick={() => dispatch1({ type: 'decrement' })}>-1</button>
        <button onClick={() => dispatch1({ type: 'set', count: 0 })}>reset</button>
      </div>
      <div>
        {count2}
        <button onClick={() => dispatch2({ type: 'increment' })}>+1</button>
        <button onClick={() => dispatch2({ type: 'decrement' })}>-1</button>
        <button onClick={() => dispatch2({ type: 'set', count: 0 })}>reset</button>
      </div>
    </>
  );
};
复制代码

能够看到,每一个计数器有各自的 dispatch 的方法,但共享了 reducer 方法。这个的功能和用例2是一致的。

用例4:文本输入(TextInput)

来看一个真实的例子,多个 useReducer 能各司其职。咱们以 React 原生的 input 输入组件为例,在本地状态 state 中存储文本数据。经过调用 dispatch 函数更新文本状态值。

const initialState = '';
const reducer = (state, action) => action;
复制代码

注意到每次调用 reducer,以前旧的 state 就会被丢掉。具体使用以下:

const Example04 = () => {
  const [firstName, changeFirstName] = useReducer(reducer, initialState);
  const [lastName, changeLastName] = useReducer(reducer, initialState);
  return (
    <>
      <div>
        First Name:
        <TextInput value={firstName} onChangeText={changeFirstName} />
      </div>
      <div>
        Last Name:
        <TextInput value={lastName} onChangeText={changeLastName} />
      </div>
    </>
  );
};
复制代码

就是这么简单。固然你也能够添加一些校验的逻辑在里面。完整代码:

import React, { useReducer } from 'react';

const initialState = '';
const reducer = (state, action) => action;

const Example04 = () => {
  const [firstName, changeFirstName] = useReducer(reducer, initialState);
  const [lastName, changeLastName] = useReducer(reducer, initialState);
  return (
    <>
      <div>
        First Name:
        <TextInput value={firstName} onChangeText={changeFirstName} />
      </div>
      <div>
        Last Name:
        <TextInput value={lastName} onChangeText={changeLastName} />
      </div>
    </>
  );
};

// ref: https://facebook.github.io/react-native/docs/textinput
const TextInput = ({ value, onChangeText }) => (
  <input type="text" value={value} onChange={e => onChangeText(e.target.value)} />
);

export default Example04;
复制代码

用例5:Context

有些时候,我但愿在组件之间共享状态(理解为实现全局状态 state )。一般,全局状态会限制组件的复用,所以咱们首先考虑使用本地 state,经过 props 进行传递( dispatch 来改变),但当它不是那么方便的时候(理解嵌套传递过多),就可使用 Context。若是你熟悉 Context 的 API,请点击查看官方文档

这个例子,使用了和用例3同样的 reducer。接下来看怎么建立一个 context。

const CountContext = React.createContext();

const CountProvider = ({ children }) => {
  const contextValue = useReducer(reducer, initialState);
  return (
    <CountContext.Provider value={contextValue}>
      {children}
    </CountContext.Provider>
  );
};

const useCount = () => {
  const contextValue = useContext(CountContext);
  return contextValue;
};
复制代码

useCount 就是自定义的 hook,它也和其余官方的 hook 同样使用。以下面同样:

const Counter = () => {
  const [count, dispatch] = useCount();
  return (
    <div>
      {count}
      <button onClick={() => dispatch({ type: 'increment' })}>+1</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-1</button>
      <button onClick={() => dispatch({ type: 'set', count: 0 })}>reset</button>
    </div>
  );
};
复制代码

contextValue就是 useReducer 返回的结果,咱们也用 hook 重构了useCount。注意到这个点,使用了哪个 context是不固定的。

最后,像这样使用 context:

const Example05 = () => (
  <>
    <CountProvider>
      <Counter />
      <Counter />
    </CountProvider>
    <CountProvider>
      <Counter />
      <Counter />
    </CountProvider>
  </>
);
复制代码

如上所示,有两个 CountProvider 组件,意味着有两个计数器,即便咱们只使用了一个 context。 在同一个 CountProvider 组件中计数器共享状态 state。你能够运行一下这个用例了解它是怎么工做的。点击这里查看

用例6:Subscription (订阅)

在hooks中实现组件共享状态 state 首选的应该就是 Context,但当在 React 组件的外部早已经有一个共享状态 state 时,该怎么(共享呢)?专业的作法订阅监听状态 state,当共享状态 state 更新时,更新组件。固然它还有一些局限性,不过React官方提供了一个公共功能 create-subscription,你能够用它来进行订阅。

不幸的,这个公共方法包尚未用 React Hooks 进行重写,如今只能靠咱们本身用 hooks 尽力去实现。让咱们不使用 Context 实现和用例 5 同样的功能。

首先,建立一个自定义的hook:

const useForceUpdate = () => useReducer(state => !state, false)[1];
复制代码

这个 reducer 仅仅是对先前的 state 取反,忽略了 action。[1]仅仅返回了 dispatch 而没有 state。接下来,主函数实现共享状态 state 以及返回自定义 hook:

const createSharedState = (reducer, initialState) => {
  const subscribers = [];
  let state = initialState;
  const dispatch = (action) => {
    state = reducer(state, action);
    subscribers.forEach(callback => callback());
  };
  const useSharedState = () => {
    const forceUpdate = useForceUpdate();
    useEffect(() => {
      const callback = () => forceUpdate();
      subscribers.push(callback);
      callback(); // in case it's already updated const cleanup = () => { const index = subscribers.indexOf(callback); subscribers.splice(index, 1); }; return cleanup; }, []); return [state, dispatch]; }; return useSharedState; }; 复制代码

咱们使用了 useEffect。它是很是重要的 hook,你须要仔细的看官方文档学习如何使用它。在 useEffect 中,咱们订阅了一个回调函数来强制更新组件。在组件销毁的时候须要清除订阅。

接下来,咱们能够建立两个共享状态 state。使用了和用例5,用例3同样的 reducer 和初始值 initialState:

const useCount1 = createSharedState(reducer, initialState);
const useCount2 = createSharedState(reducer, initialState);
复制代码

这和用例 5 是不同的,这两个 hooks 绑定了特定的共享状态 state。而后咱们使用这两个 hooks。

const Counter = ({ count, dispatch }) => (
  <div>
    {count}
    <button onClick={() => dispatch({ type: 'increment' })}>+1</button>
    <button onClick={() => dispatch({ type: 'decrement' })}>-1</button>
    <button onClick={() => dispatch({ type: 'set', count: 0 })}>reset</button>
  </div>
);

const Counter1 = () => {
  const [count, dispatch] = useCount1();
  return <Counter count={count} dispatch={dispatch} />
};

const Counter2 = () => {
  const [count, dispatch] = useCount2();
  return <Counter count={count} dispatch={dispatch} />
};
复制代码

注意到,Counter 组件是一个共同的无状态组件。这样子使用:

const Example06 = () => (
  <>
    <Counter1 />
    <Counter1 />
    <Counter2 />
    <Counter2 />
  </>
);
复制代码

能够看到,咱们没有用 Context,可是也实现了共享状态。你们应该都要具体看看 useReducer,对于性能优化颇有帮助。

文中全部的代码都在这里,要看在线示例点击这里查看

本身的一些总结:

  1. hooks每次 Render 都有本身的 Props 与 State 能够认为每次 Render 的内容都会造成一个快照并保留下来(函数被销毁了,但变量被react保留下来了),所以当状态变动而 Rerender 时,就造成了 N 个 Render 状态,而每一个 Render 状态都拥有本身固定不变的 Props 与 State。 这也是函数式的特性--数据不变性
  2. 性能注意事项 useState 函数的参数虽然是初始值,但因为整个函数都是 Render,所以每次初始化都会被调用,若是初始值计算很是消耗时间,建议使用函数传入,这样只会执行一次:
  3. 若是你熟悉 React 的 class 组件的生命周期,你能够认为useEffect Hook就是组合了componentDidMount, componentDidUpdate, 以及 componentWillUnmount(在useEffect的回调中),可是又有区别,useEffect不会阻止浏览器更新屏幕
  4. hooks 把相关的逻辑放在一块儿统一处理,不在按生命周期把逻辑拆分开
  5. useEffect 是在浏览器 render 以后触发的,想要实现 DOM 改变时同步触发的话,得用 useLayoutEffect,在浏览器重绘以前和 DOM 改变一块儿,同步触发的。不过尽可能在布局的时候,其余的用标准的 useEffect,这样能够避免阻止视图更新。
```
function FunctionComponent(props) {
  const [rows, setRows] = useState(() => createRows(props.count));
}
useRef 不支持这种特性,须要写一些[冗余的函断定是否进行过初始化。](https://reactjs.org/docs/hooks-faq.html#how-to-create-expensive-objects-lazily)
```
复制代码

若是有错误或者不严谨的地方,请务必给予指正,十分感谢!

参考

  1. 精读《useEffect 彻底指南》
  2. react 官网
相关文章
相关标签/搜索