解析常用的React Hook

467 阅读5分钟

这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战

HookReact 16.8 的新特性,它可以让我们在函数式组件中使用state

为什么有了Hook

组件之间的复用状态逻辑

React 是构建用户界面的 JavaScript 库,可以轻松的通过 声明式组件化 的形式创建交互式 UI视图。它本事是注重 UI 层面的。

对于组件状态的复用,通常都是通过providersconsumersrender props高阶组件,将一个个组件嵌套起来的,此时如果需要组织或者重构代码,你会发现是个很难的事情。这也是后来 redux 中大型项目中比较流行的原因。为了解决这个共享状态逻辑的痛点,Hook 的诞生,为你在无需修改组件结构的情况下复用状态逻辑提供了更加原生方式。

最常见使用方式就是把一些公共的逻辑状态提取到一个自定义的 hook 中:

function useLoadingStatus(fetch) {
  const [isLoading, setIsLoading] = useState(null);

  // ...

  return isLoading;
}

组件的复杂度高,难以拆分为小的颗粒

class组件中,我们需要用到大量的生命周期函数,比如componentDidUpdatecomponentDidMount等等,对复杂代码的拆分变的异常困难,而且很容易产生 bug,大多情况下,很难将组件拆分成很多很小粒度的组件,即使是使用 redux 类的状态管理库。也正是为了解决这个问题,Hook 将组件中相互关联的部分拆分成更小的函,如设置订阅请求数据,而并非强制按照生命周期划分。同时我们还可以使用reducer来管理内部的状态,是状态更加的可以预测。

比较常见的就是我们可以使用多个 useEffect 将不同的逻辑分开处理,我们只需维护好它们各自的依赖([])。

下面是我自己做的一个倒计时的控制:

/** *
* @param {amountTime} 时间单位 s
* @param {idx} 当前题目
* @param {handleSubmit} 提交答案
*/
function CountTime({ amountTime, idx, handleSubmit }) {
  const [time, setTime] = useState(amountTime);
  useEffect(() => {
    setTime(amountTime);
  }, [idx, amountTime]);

  useEffect(() => {
    if(time === 0){
      handleSubmit && handleSubmit(1);
      return;
    }
    const timer = setTimeout(() => {
      if (time > 0) {
        setTime(time - 1);
      } else {
        handleSubmit();
      }
    }, 1000);

    // eslint-disable-next-line consistent-return
    return () => timer && clearTimeout(timer);
  }, [time]);


  return <Text style={styles.amountNumber}>{time}s</Text>;
}

常用的Hook

useState

useState() 的功能类似 class 组件的 statesetState(),使我们可以在函数式的组件中使用state,也为组件的拆分提供了方便、

const [state, setState] = useState(initialState);

另外 state 的初始化还有一个种 惰性初始化,依赖 props 进行初始化。
initialState 只会在组件的初始渲染中起作用,后续渲染时会被忽略。
const [state, setState] = useState(() => {
  const initialState = someExpensiveComputation(props);
  return initialState;
});

如果要更新state, 直接在合适的时机调用 setState(newState) 就可以了。

还有一种常见的通过函数计算的更新:新的state值需要通过当前的值计算得出的

setState(state => state + 1)

useEffect

useEffect 的心智模型和 componentDidMount 以及其他生命周期函数式不同的。它的心智模型更接近于状态同步,而不是响应生命周期事件

它一般使用了执行一个带有副作用的函数,传给 useEffect 的函数会在浏览器完成布局与绘制之后,在一个延迟事件中被调用,但是它总会在任何新的渲染前执行。 说到这里不得不提一下 useLayoutEffect,它和useEffect相似,但它会在所有的 DOM 布局完成之后同步调用 effect触发重新渲染。通常用来获取元素的位置,比如scroll的值。

通常在组件卸载的时候需要清除一些副作用,useEffect 函数只需返回一个清除函数即可。

useEffect 第二个参数接受一个数组作为它重新执行辅佐的依赖,只有数组内的值发生改变,才会重新执行。

可以参考此文useEffect指南

useCallback

我们通常把一个回调函数回调函数依赖放到 useCallback 中,它会返回一个 memoized 回调函数,当回调函数依赖发生变化时,重新执行回调函数。

这里就有一个很常见的性能优化:

父组件的方法通过props传给了子组件时,我们将父组件的方法通过 useCallback 钩子包一下,同时需要将钩子的依赖设置为[]空依赖。这样就可以减少子组件的rerender次数

useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。

useMemo

它的作用和 useCallback 有点类似。把函数依赖项数组作为参数传入useMemo,它返回一个 memoized 值。它也是一种渲染性能优化,传入 useMemo 的函数会在渲染期间执行。

如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。

useRef

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内持续存在。通常作为访问DOM的方式

const refInput = useRef(null);
...
...
return(
  <input ref={(r) => refInput = r} type="text" />
)

ref 对象内容发生变化时,变更 .current 属性,但是不会引发组件重新渲染。所以有时候还可以用来保存一些额外的数据。

react Hooks性能优化

减少rerender次数

当父组件重新属性更新触发重新渲染的时候,子组件也会重新渲染

  1. 其实我们的子组件属性没有任何变化,此时我们不希望子组件重新渲染,我们可以将子组件使用memo将子组件包一下。
  2. 当父组件的方法通过props传给了子组件时,对子组件只使用memo是不行的,我们需要将父组件的方法通过useCallback钩子包一下,同时需要将钩子的依赖设置为[]空依赖。

减少rerender复杂度

使用memoize-one,来减少数据计算

const add = (a, b) => a + b;
const memoizedAdd = memoizeOne(add);

结语

常用的hook就介绍到这里,如果这篇文章帮到了你,欢迎点赞👍和关注⭐️

文章如有错误之处,希望在评论区指正🙏🙏。