React进阶:什么时候使用useState和useReducer

4,208 阅读10分钟

  react hooks已经发布很久了,它允许我们在函数式组件中拥有私有状态和副作用,react hook主要提供了2个hook:useState和useReduce来管理React组件中的状态,这个教程不会详细介绍所有React hook的细节,它会解释在不同的场景下如何使用这两个hook。


先看一个例子,分别使用useState和useReduce来实现管理一个计数器组件的状态:

useState应该是最先接触到的hook,它提供初始值渲染组件,并返回一个state状态值和一个更新state的updater函数来更新函数组件状态

//by useState
import React, { useState } from 'react';
 
const Counter = () => {
  const [count, setCount] = useState(0);
 
  const handleIncrease = () => {
    setCount(count => count + 1);
  };
 
  const handleDecrease = () => {
    setCount(count => count - 1);
  };
 
  return (
    <div>
      <h1>Counter with useState</h1>
      <p>Count: {count}</p>
 
      <div>
        <button type="button" onClick={handleIncrease}>
          +
        </button>
        <button type="button" onClick={handleDecrease}>
          -
        </button>
      </div>
    </div>
  );
};
 
export default Counter;

useReduce 也可以用来更新状态,但是它会更加复杂一些,它接收一个初始状态和一个reducer纯函数作为参数,并且返回一个实际状态和一个操作reducer的dispatch函数,

//by useReducer
import React, { useReducer } from 'react';

const counterReducer = (state, action) => {
  switch (action.type) {
    case 'INCREASE':
      return { ...state, count: state.count + 1 };
    case 'DECREASE':
      return { ...state, count: state.count - 1 };
    default:
      throw new Error();
  }
};
 
const Counter = () => {
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });
 
  const handleIncrease = () => {
    dispatch({ type: 'INCREASE' });
  };
 
  const handleDecrease = () => {
    dispatch({ type: 'DECREASE' });
  };
 
  return (
    <div>
      <h1>Counter with useReducer</h1>
      <p>Count: {state.count}</p>
 
      <div>
        <button type="button" onClick={handleIncrease}>
          +
        </button>
        <button type="button" onClick={handleDecrease}>
          -
        </button>
      </div>
    </div>
  );
};
 
export default Counter;

上面两个函数组件使用不同的hook管理state,他们解决了相同的业务问题,但是使用了不同的方式,那么何时使用useState何时使用useReducer呢?


简单vs复杂的state

reducer把简单的count属性封装进了一个state object,但是我们将count作为一个实际state会更加简单,重构这个state object把count设为一个JavaScript的int类型变量,我们可以发现这个例子不涉及管理复杂的状态;

在这个例子里,因为没有复杂的state 对象,我们最好使用useState这个hook会更简单,因此我们可以把state对象重构为一个基础类型变量

无论如何,我必须强调的是当你不是需要管理基础类型(string,integer,boolean)变量,而是需要管理复杂类型的object对象时(array,object),你最好使用useReducer,也许下面这两条是一个非常好的经验规:

  • 当用于管理基础类型js变量时使用useState
  • 当使用object对象或者array这类引用类型时使用useReducer


这个经验法则告诉我们,当你在代码中写到类似:const [state, setState] = useState({ firstname: 'Robin', lastname: 'Wieruch' }) 时,你最好使用useReduce代替useState;


简单vs复杂的state状态变化过程

如果我们在之前的状态变化中没有使用两种不同的 action 类型(INCREASE and DECREASE),使用单个的action会有什么不同? 通过给每个dispatiched对象上添加可选的payload参数,我们可以从reducer的外部指定要添加或者减少的count值,这使得state的变化过程变得更加隐式;

import React, { useReducer } from 'react';
 
const counterReducer = (state, action) => {
  switch (action.type) {
    case 'INCREASE_OR_DECREASE_BY':
      return state + action.by;
    default:
      throw new Error();
  }
};
 
const Counter = () => {
  const [count, dispatch] = useReducer(counterReducer, 0);
 
  const handleIncrease = () => {
    dispatch({ type: 'INCREASE_OR_DECREASE_BY', by: 1 });
  };
 
  const handleDecrease = () => {
    dispatch({ type: 'INCREASE_OR_DECREASE_BY', by: -1 });
  };
 
  return (
    <div>
      <h1>Counter with useReducer</h1>
      <p>Count: {count}</p>
 
      <div>
        <button type="button" onClick={handleIncrease}>
          +
        </button>
        <button type="button" onClick={handleDecrease}>
          -
        </button>
      </div>
    </div>
  );
};
 
export default Counter;

但是我们没有这没做,这是一个关于使用reducers非常重要的一课:始终要尝试去通过reducer解释你的state变换过程,在上面代码的例子中只存在一个state的状态变换却试图把整个转换逻辑放进一个switch块中,这是非常不理想的使用reducer的方式,相反的,我们希望可以非常轻松的去推导出state的变换过程,使用2个独立的状态转换代码块(而非一个)可以让我们通过阅读action的type name更容易的推导state变换的业务逻辑

useReducer相比于useState给了我们关于state的变化过程更多的可预测性,这对于state变得更加复杂并且你希望又一个地方(reducer 函数)去推导他们的变化过程是非常重要的,一个设计优秀的reducer 函数可以把业务逻辑封装的非常好;

一个新的经验准则:当你发现连续多个setState()这样的updater函数调用时,尝试着去将这些代码压缩进一个reducer函数里的一个单独的action代码块中(switch);

另外对于使用useReducer管理一个state object的另外一个非常大的优势是当你需要用 浏览器的 local storage等本地缓存去缓存你的也页面state状态,并在下一次重启应用时作为初始状态会非常便捷~


在state Object上操作复杂的state变换

当你的应用成长到一定规模时,你可能会需要解决越来越多复杂的state和state的状态变换,这是本文后面的部分将要讨论的内容。特别需要注意的是,尽管state object没有变得非常复杂,它指示增加了几个state 变换需求需要去执行;

例如,下面这个例子里的reducer需要对一个state object对象进行多个state变换(多种action)的操作:

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;
        }
      });
    case 'ADD_TODO':
      return state.concat({
        task: action.task,
        id: action.id,
        complete: false,
      });
    default:
      throw new Error();
  }
};

它仅仅对于将所有内容放进一个state oject(比如一个待办列表)同时对该对象进行多种状态变换的操作时才有意义;他会比使用useState实现相同的业务逻辑更加的可维护可预测;


随着时间的推移,你通常会随着state object变得越来越复杂并且状态变换的场景不断的增加开始将状态管理的方式从useState过度到使用useReducer,在其他情况下,将多种不同的属性放进一个单独state object中也是有意义的,及时他们最开始并不是放在一起的,比如 这篇 如何使用useEffect,useState,useReducer获取数据的教程 中将多个互相依赖的属性放到一个state object中

const [state, dispatch] = useReducer(dataFetchReducer, {
  isLoading: false,
  isError: false,
  data: initialData,
});

可能有人会认为把isLoading和isError两个状态使用useState单独管理,但是当你看下面的reducer函数时你会发现最好把他们放到一个state object中,因为他们有条件的就互相依赖;

const dataFetchReducer = (state, action) => {
  switch (action.type) {
    case 'FETCH_INIT':
      return {
        ...state,
        isLoading: true,
        isError: false
      };
    case 'FETCH_SUCCESS':
      return {
        ...state,
        isLoading: false,
        isError: false,
        data: action.payload,
      };
    case 'FETCH_FAILURE':
      return {
        ...state,
        isLoading: false,
        isError: true,
      };
    default:
      throw new Error();
  }
};

不仅是 state oject的复杂程度和state变换类型的多少非常重要,还必须考虑在高效的管理state状态时如何把应用上下文环境中的属性合理组合,如果使用useState将每个逻辑都放在多个不同的代码快进行管理,他将很快变成一个难以维护逻辑单元,另一个非常重要的提升开发体验的优势是,使用一个代码块(reducer 函数)管理state object的多个状态变换,当出现错误任何错误时你都能更加容易的去debug你的逻辑代码;


将所有的状态变换井井有条的放进一个reducer函数另一个非常大的优点时非常有利于进行单元测试,这将在你需要对拥有多个状态变换场景的state object的每个状态变换需求进行测试时变得更容易推理变换过程,用一个:(state,action)=》newState 函数,你可以使用reducer提供的多个action type和poayload参数来测试所有的状态变换场景,action仅仅提供了非常少的信息


状态变换的逻辑

在使用useState或者useReducer时,状态变换的逻辑放在哪里有很大的区别,我们可以看这个useReducer的例子,逻辑代码被放在了reducer函数中,仅仅提供了非常少的信息给action,就执行了状态变换操作获得了当前的状态:

(state,action)=》new State,如何你依赖当前状态去更新状态会非常便捷;

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;
        }
      });
    case 'ADD_TODO':
      return state.concat({
        task: action.task,
        id: action.id,
        complete: false,
      });
    default:
      throw new Error();
  }
};

你的React componment组件只需要关心如何分发何时的action~

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

现在请想象一下当你使用useState来完成相同的state变换,在那样的情况下,没有一个像reducer这样的单一入口可以集中处理所有的业务逻辑,取而代之的是使用从useState处获取的updater函数将所有的状态逻辑都放在单独的处理程序中,这使得状态逻辑和视图逻辑的分离变得更加困难,从而增加了组件的复杂度,因此,reducer是一个收集所有修改state操作逻辑的绝佳场所;


触发状态变化

React 组件树会随着你的应用逐渐增涨,当状态(状态和触发状态的方式)是简单的并且被封装在一个组件中时,比如在受控组件中输入查询字段的场景,useState会更更佳合适:

import React, { useState } from 'react';
 
const App = () => {
  const [value, setValue] = useState('Hello React');
 
  const handleChange = event => setValue(event.target.value);
 
  return (
    <div>
      <label>
        My Input:
        <input type="text" value={value} onChange={handleChange} />
      </label>
 
      <p>
        <strong>Output:</strong> {value}
      </p>
    </div>
  );
};
 
export default App;

尽管如此,当你像在最顶层的组件中管理你的state状态但是触发组件修改的位置处于组件树的深处时,你可能会通过props来传递useState的updater函数或者useReducer的dispatch调度函数,但是使用 React 的 context API在这种场景下会显得更佳合适,与使用来自useState的多个必须单独传递的updater函数相比,使用一个具有多种actions和payloads的dispatch函数会是更好的选择,dispatch函数可以通过React 的useContext hook乡下传递,这个教程里有很好的使用useContext的例子~

是否使用useState或者useReducer的结论并不是非黑即白的,我希望本文能够让更好的了解何时使用useState或者useReducer~下面是本文的重点:

总结

以下情况使用useState:

  • state是javascript基础类型
  • state变化很简单
  • 业务逻辑在组件内就能完成
  • 多个state间的变化没有相互关系可以用多个useState进行管理
  • state和你的组件是耦合的 co located,共处同一个组件中
  • 一个很小的应用(边界模糊,视自己的情况而定)


什么时候使用reducer

  • state是数组或者对象
  • 复杂的state变化
  • 复杂的业务逻辑更适合用reducer 函数
  • 不同的属性被捆绑在了一起必须使用一个state object对象进行统一管理
  • 需要更新更深组件树更深层次的state
  • 中等大小的应用(边界模糊)
  • 需要更便捷的测试
  • 需要更加可以预测和可以维护的state架构