React hooks使用经验

1,808 阅读8分钟

useState

useState接收的值只作为初始渲染时的状态,后续的重新渲染的值都是通过setState去设置

1. 函数式更新

除了常规的setState(value)的方式去更新状态以外,setState还可以接收一个函数来更新状态。这种更新状态的方式通常使用在新的 state 需要通过使用先前的 state 计算得出的场景。

在effect 的依赖频繁变化的场景下有时也可以通过函数式更新状态来解决问题,例如一个每秒中自增状态的场景:

但是依赖项设置后会导致每次改变发生时定时器都被重置,这并不是我们想要的,所以这时就能够通过函数式更新状态并且不引用当前state。

2. 惰性初始 state

一些需要复杂计算的初始状态如果直接将函数运行结果传入useState,会在每次重新渲染时执行所传入的函数,比如:

codesandbox

所以可以向useState中传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用codesandbox

useEffect

useEffect用来完成副作用。

1. 处理副作用与class组件的区别

在class组件中,通常做法是在生命周期中去检查props.A 和 state.B,如果符合某个条件就去触发XXX副作用。而在function组件中,思维模式是我需要处理XXX副作用,它的依赖数据是props.A 和 state.B。从之前的命令式转变到function组件的声明式,开发者不再需要将精力投入到思考不同生命周期判断各种条件再执行副作用这样的事中,而可以将精力聚焦在更高的抽象层次上。

2. 需要将effect中用到的所有组件内的值都要包含在依赖中

React在每次渲染都有这次对应的state、props、事件处理函数和effect,如果设置了错误的依赖就可能会导致副作用函数中所使用到的值并不是最新的。

举个例子,我们来写一个每秒递增的计数器。在Class组件中,我们的直觉是:“开启一次定时器,清除也是一次”。这里有一个例子说明怎么实现它。当我们理所当然地把它用useEffect的方式翻译,直觉上我们会设置依赖为[],因为“我只想运行一次effect”。

然而,这个例子只会递增一次。因为副作用函数只在第一次渲染时候执行,第一次渲染对应的count是0,所以定时器中永远是setCoun(1)。

依赖项是我们给react的提示,告诉react不必每次渲染都去执行effect,只需要在依赖项变动时才去执行对应的effect。错误的依赖项导致effect中拿到的状态的值可能跟上一次effect执行时一样而不是最新的状态。

类似于这样的问题是很难被想到的,所以需要启用 eslint-plugin-react-hooks 中的 exhaustive-deps 规则,此规则会在添加错误依赖时发出警告并给出修复建议。

3. 优化依赖项

尽量设置少的依赖项

在effect内部去声明它所需要的函数

比如在某些情况下,组件内函数和effect依赖同一个state

因为doSomething这个函数并没有被使用到多个地方,所以可以将它声明到effect内部去减少依赖项

函数使用useCallback去包裹

和上面一种情况类似,但是doSomething这个函数在多个地方使用

这种情况不方便把一个函数移动到 effect 内部,可以将函数使用useCallback去包裹这个函数

将函数声明到组件外部

如果函数没有使用到组件内的值,可以将函数声明到组件外部以减少依赖项

使用函数式更新状态来减少依赖项

比如在依赖当前状态来更新状态的情况下,可以使用函数式更新状态来减少依赖项,就像上面useState中所举的例子一样。

useRef

1. 使用useRef保存组件中所需要的的唯一实例对象

比如在function组件中去使用rxjs数据流时,需要在组件挂载和销毁时监听和取消监听,如果在组件外去定义subject,全局监听的都是同一个observable

这样在其中任一个组件中next值时,都会被所有观察者接收到。

所以这里需要保持每个组件有自己独立的observable,并且它又不需要作为状态去参与渲染,所以这块使用useRef去保存这个observable。

这样就达到了目的。

2. 保存一些不参与渲染的值

当 ref 对象内容发生变化时,useRef 并不会通知你。变更 .current 属性不会引发组件重新渲染,所以我们可以使用useRef去保存一些不参数渲染的值。

3. 如果需要直接修改useRef的结果,则在泛型参数的类型中包含 | null

在给.current直接赋值时,ts会给出错误提示:

image.png

那么怎么将 current 属性转为 动态可变 的呢,其实在 useRef 的类型定义中已经给出了答案。

image.png

如果需要直接修改useRef的结果,则在泛型参数的类型中包含| null就可以了。

useMemo useCallback

useMemo和useCallback一起作为组件渲染优化的选择而出现,但是它们不能作为性能优化的银弹而去在任何情况下去使用。

我们需要知道这两个api本身也有开销。它们 会「记住」一些值,同时在后续 render 时,将依赖数组中的值取出来和上一次记录的值进行比较,如果不相等才会重新执行回调函数,否则直接返回「记住」的值。这个过程本身就会消耗一定的内存和计算资源。因此,过度使用 useMemo/useCallback 可能会影响程序的性能。

所以要想合理使用 useMemo/useCallback,我们需要搞清楚 它们 适用的场景:

  • 有些计算开销很大,我们就需要「记住」它的返回值,避免每次 render 都去重新计算。

  • 由于值的引用发生变化,导致下游组件重新渲染,我们也需要「记住」这个值。

useReducer

useState的替代模式,在状态之间逻辑复杂时使用useReducer可以将what和how分开,只需要在组件中声明式的dispatch对应的行为,所有行为的具体实现都在reducer中维护,让我们的代码可以像用户的行为一样,更加清晰。

这是一个登录demo, 通过useReducer将登录的相关状态抽离出组件内部,防止组件内部多处去维护这些状态,组件内部只需要通过dispatch行为来完成各种交互。

如果你的页面state比较复杂(state是一个对象或者state非常多散落在各处)请使用userReducer

useContext

与useReducer结合使用,代替将回调函数作为参数向下传递的方式,改为共享context中的dispatch

改写之前的登录demo,将登录button改为子组件,通过useContext去拿到dispatch

如果你的页面组件层级比较深,并且需要子组件触发state的变化,可以考虑useReducer + useContext

useImperativeHandle

useImperativeHandle 一般与 forwardRef 一起使用。

主要作用与forwarRef基本相同,都是为了将子组件的一些数据暴露给父组件。区别在于只使用forwarRef时只能够对ref进行转发对外暴露dom元素实例,使用useImperativeHandle时能够自定义对外暴露的实例值。

// 子组件
const Bar = React.forwardRef<{}, any>((props, ref) => {
  const [value, setValue] = useState('');

  const inputRef = useRef<HTMLInputElement>(null);

  // 对外暴露出api
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current?.focus();
    },
    blur: () => {
      inputRef.current?.blur();
    },
    changeValue: (val: string) => {
      setValue(val);
    },
    instance: inputRef.current,
  }));

  return (
    <div>
      <Input ref={inputRef} value={value}></Input>
    </div>
  );
});

// 父组件
const Foo = () => {
  const ref = useRef<HTMLInputElement>(null);

  useEffect(() => {
    console.log('【ref】', ref.current);
  }, []);

  return <Bar ref={ref}></Bar>;
};

可以看到父组件中打印结果:

image.png

确实拿到了子组件中暴露出来的一些数据。

父组件可以通过子组件useImperativeHandle暴露出的api来修改子组件的状态

常规父组件控制子组件的行为都是将状态通过props传给子组件然后在父组件中去控制改状态的形式来实现,但是在很少的情况下这种方式满足不了我们的需求,例如在做一些复杂的通过方法调用而不是组建式调用的ui组件时。这时就可以用到父组件可以通过子组件useImperativeHandle暴露出的api来修改子组件的状态的方式来达到目的。

比如说上例中在父组件调用的changeValue方法就能够间接的修改子组件内部的value状态,从而达到控制子组件的效果。

ref.current.changeValue('这是改变后的值');

image.png

不过需要强调的是,这种父组件控制子组件的方式在常规需求中几乎使用不到,应当尽量避免对子组件ref的过度使用。

参考文章: