React 「Hooks」总结

708 阅读18分钟

简介

  • Hook 是可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。使用 「函数声明组件」时,用的就是Hook

  • 特点:

    • 完全可选的。可以在一些组件中尝试 Hook,但是如果你不想,你不必去学习或使用 Hook
    • 100% 向后兼容:Hook 不包含任何破坏性改动
    • 目前可用:Hook 已在 V16.8 中发布
    • 没有计划从 React 中移除 class
    • Hook 不会影响你对 React 概念的理解
      • Hook 为已知的 React 概念提供了更直接的API:props / state / context / refs 以及生命周期

动机

  • 在组件之间复用状态逻辑很难:

    React 没有提供将可复用性行为「附加」到组件的途径;常用 render props 和 高阶组件 解决此问题。但是这类方案需要重新组织你的组件结构,这可能会很麻烦,使你的代码难以理解

  • 由 providers , consumers,高阶组件,render props 等其他抽象层组成的组件会形成「嵌套地狱」

  • 使用 Hook 从组件中提取状态逻辑,使得这些逻辑可以单独测试并复用。Hook 使你在无需修改组件结构的情况下复用状态逻辑

  • 复杂组件变得难以理解

    每个生命周期常常包含一些不相关的逻辑,逐渐会被状态逻辑和副作用充斥,相互关联且需要对照修改的代码被进行了拆分,而完全不相关的代码却在同一个方法中组合在一起。如此很容易产生Bug,并且导致逻辑不一致

    Hook将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据) ,而并非强制按照生命周期划分。你还可以使用 reducer 来管理组件的内部状态,使其更加可预测。

  • 难以理解的 class

    Hook 使你在非 class 的情况下可以使用更多的 React 特性。

    Hook 和现有代码可以同时工作,你可以渐进式地使用他们

概览

  • Hook 是一些可以让你在函数组件里「钩入」React state 及生命周期等特性的函数
  • Hook 不能在 class 组件中使用 —— 这使得你不使用 class 也能使用 React
  • Hook 已保存在函数作用域中,可以方便访问 state/props 等变量
  • Hook 使用了 JS 的闭包机制,而不用在 JS 已经提供了解决方案的情况下,还引入特定的 React API。

state Hook

  • 在函数组件里调用它来给组件添加一些内部的 state。React 会在重复渲染时保留这个 state
  • useState 会返回一对值:当前状态和一个让你更新它的函数,你可以在事件处理函数中或其他一些地方调用这个函数
  • useState 唯一的参数就是初始 state。这个初始参数只有在第一次渲染时会被用到。
  • 可以在一个组件中多次使用 State Hook

Effect Hook

  • React 会等待浏览器完成画布渲染之后才会延迟调用 useEffect
  • useEffect 就是一个 Effect Hook,给函数组件增加了操作副作用的能力。
    • 合并了 class 的 componentDidMount componentDidUpdatecomponentWillUnmount
    • 通过使用 Hook ,你可以把组件内相关的副作用组织在一起,而不要把它们拆分到不同的生命周期函数里
  • 在React组件中执行数据获取、订阅或者手动修改DOM。我们统一把这些操作称为「副作用」,或者简称为「作用」
  • 由于副作用函数是在组件内声明的,所以它们可以访问到组件的 props 和 state
  • 默认情况下,React 会在每次渲染后调用副作用函数 —— 包括第一次渲染的时候
  • 副作用函数还可以通过 return 返回一个函数来指定如何「清除」副作用
  • 可以在组件中多次使用 useEffect

Hook 使用规则

Hook 就是 JS 函数,但是使用它们会有两个额外的规则 :

  • 只能在 函数最外层 调用Hook。不要在循环、条件判断或者子函数中调用。
  • 只能在 React的函数组件 中调用Hook。不要在其他 JS 函数中调用(自定义的Hook中也可以调用)

自定义 Hook

  • 自定义Hook可以在不增加组件的情况下达到 想要在组件之间重用一些状态逻辑 的目的。来替代两种主流方案: 高阶组件 和 render props
  • 我们只需要将调用 useState 和 useEffect 的 Hook 逻辑抽取到一个自定义的 Hook 即可。
  • 可以在单个组件中多次调用同一个自定义 Hook(Hook 的每次调用都有一个完全独立的 state)
  • 自定义 Hook 更像是一种约定而不是功能。如果函数的名字以「use」开头并调用其他 Hook,我们就说这是一个自定义的Hook(更方便在检查工具中找到问题所在,如 linter 插件)

其他 Hook

  • useContext 让你不使用组件嵌套就可以订阅 React 和 Context
  • useReducer 可以让你通过 reducer 来管理组件本地的复杂 state

useState

  • useState 与 class 里面的 this.state 提供的功能完全相同。一般来说,在函数退出后变量就会「消失」,而 state 中的变量会被 React 保留
  • useState() 方法里面唯一的参数就是初始 state(参数与 class 不同的是可以是基础数据类型,而不一定是对象)
  • useState() 的返回值为:当前 state 以及 更新state的函数,需要成对的获取它们
const [count , setCount] = useState(0);
// JS 语法叫 数组解构 ,它意味着我们同时创建了两个变量,第一个变量返回的是第一个值,第二个变量返回的是第二个值。

Effect Hook

  • Effect Hook 可以让你在函数组件中执行副作用操作。
    • 数据获取,设置订阅以及手动更改 React 组件中的DOM都属于副作用
  • 每次重新渲染都会生成新的 effect 替换掉之前的
    • 某种意义上讲,effect 更像是渲染结果的一部分 —— 每个 effect '属于' 一次特定的渲染。
    • 传递给 effect 的函数在每次渲染中都会有所不同,因此我们可以在 effect 中获取到最新的数据值,而不用担心其过期的原因
  • useEffect 会在每次渲染后都执行(在第一次渲染之后和每次更新之后都会执行)
  • 可以把 useEffect Hook 看作是 componentDidMountcomponentDidUpdatecomponentWillUnmount 这三个函数的组合。(避免在多个生命周期函数中编写重复的代码)
  • React 组件中有两种常见的副作用操作:需要清除的和不需要清除的
    • 无需清除的 effect:在React更新DOM之后运行一些额外的代码,执行完这些操作之后,就可以忽略它们了(如发送网络请求,手动变更 DOM ,记录日志)
    • 需要清除的 effect:例如订阅外部数据源,清除工作是非常重要的,可以防止引起内存泄露
      • 每个 effect 都可以返回一个清除函数。如此可以将添加和订阅的逻辑放在一起,它们都属于 effect 的一部分
      • React 会在组件卸载的时候执行清除操作。而 effect 在每次渲染都会执行。这就是为什么React会在执行当前 effect 之前对上一个 effect 进行清除。每次都重新渲染新的 effect。

加强

  • 使用多个Effect实现关注点分离
    • Hook 允许我们按照代码的用途分离他们,而不是像生命周期函数那样。React 将按照 effect 声明的顺序依次调用组件中的每一个 effect
    • 解决 class 中生命周期函数经常包含不相关的逻辑,但又把相关逻辑分离到了几个不同的方法中的问题
  • 每次更新的时候都要运行 Effect
    • useEffect会默认在调用一个新的 effect 之前对前一个 effect 进行清理。避免了组件函数中的内存泄漏造成了Bug问题
  • 通过路过 Effect 进行性能优化
    • 在某些情况下,每次渲染后都执行清理或者执行 effect 可能会导致性能问题。如果 某些特定值 在两次重渲染之间没有发生变化,你可以通知 React 路过 对 effect 的调用,只要传递数组作为 useEffect 的第二个可选参数 。

    • 同样对于有清除操作的effect同样适用

    • 确保数组中包含了 所有对外部作用域中会随时间变化并且在 effect 中使用的变量 , 否则你的代码会引用到先前渲染中的旧变量

    • 如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组 [] 作为第二个参数。

Hook 规则

  • 只在最顶层使用 Hook
    • 确保Hook在每一次渲染中都按照同样的顺序被调用,保持Hook状态的正确
      • React 靠的是Hook调用的顺序,来确定哪个 state 对应哪个 useState
      • 若不在顶层使用 Hook 则有可能破坏顺序,导致state状态出现bug
    • 不要在循环、条件或嵌套函数中调用Hook,确保总是在你的React函数的最顶层以及任何 return 之前调用他们
  • 只在 React 函数中调用Hook
    • 确保组件的状态逻辑在代码中清晰可见
    • 不要在普通的 JS 函数中调用Hook

Hook API

基础Hook

useState

const [count , setCount] = useState(0);
  • 在初始渲染期间,返回的状态与传入的第一个参数值相同
  • setState 函数用于更新 state。它接收一个新的 state 值并将组件的一次渲染加入队列。
    • 两种用法:普通式和函数式
      • setCount(x):直接传入参数值,设置 state 状态
      • setCount(x => x+1) 调用函数改变state状态
  • setState 函数的标识是稳定的,并且不会在组件重新渲染时发生变化
  • 如果你的更新函数返回值与当前 state 完全相同,则随后的重渲染会被完全跳过
  • useState 不会自动合并更新对象
    • 可以用函数式的 setState 结合展开运算符来达到合并更新对象的效果
    return {...prevState , ...updatedValues}
    
  • 惰性初始 state:初始 state 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用
  • 跳过state更新:调用 state hook 的更新函数并传入当前state时,React 将跳过子组件的渲染及effect的执行
    • React 使用 Object.is 比较算法来比较 state
    • React 可能仍需要在跳过渲染前渲染该组件
    • 如果你在渲染期间执行了高开销的计算,则可以使用 useMemo 来进行优化

useEffect

useEffect(didUpdate)
  • 该 Hook 接收一个包含命令式、且可能有副作用代码的函数
  • 使用 useEffect 完成副作用操作,赋值给 useEffect 的函数会在组件渲染到屏幕之后执行。
  • 默认情况下,effect 将在每轮渲染结束后执行,但你可以选择让它 只有某些值改变的时候 才执行
  • 为防止内存泄漏,清除函数会在组件卸载前执行
    • 如果组件多次渲染(通常如此)则在执行下一个 effect 之前,上一个 effect 就已被清除
  • useEffect 的函数与 componentDidMount、componentDidUpdate 不同,在浏览器完成布局与绘制之后执行
  • React 为此提供了额外的 useLayoutEffect Hook 来处理不能被延迟执行的effect。它和 useEffect 的结构相同,区别只是调用的时机不同(类似于被动监听事件和主动监听事件的区别)
  • 可以给 useEffect 传递第二个参数,它是 effect 所依赖的值数组,只有当值数组改变后才会重新渲染执行。
    • 所有effect函数中引用的值都应该出现在依赖项数组中,因为依赖项数组不会作为参数传递给 effect 函数
    • 未来编译器会更加智能,届时自动创建数组将成为可能。

useContext

const value = useContext(MyContext);
  • 接收一个 context 对象(React.createContext的返回值)并返回该 context 的当前值。
  • useContext 的参数必须是 context 对象本身:
    • 正确: useContext(MyContext)
    • 错误: useContext(MyContext.Consumer)
    • 错误: useContext(myContext.Provider)
  • 当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定
  • 祖先使用 React.memo 或 shouldComponentUpdate ,也会在组件本身使用 useContext 时重新渲染。

额外的 Hook

额外的Hook是上一节中基础 Hook 的变体,有些则仅在特殊情况下会用到

useReducer

const [state ,dispatch] = useReducer(reducer , initialArg , init);
  • useState 的替代方案。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。

  • 使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数 。

  • 惰性初始化:将 init 函数作为 useReducer 的第三个参数传入,这样初始 state 将被设置为 init(initialArg)。

  • 跳过 dispatch:如果 Reducer Hook 的返回值与当前 state 相同,React 将跳过子组件的渲染及副作用的执行。

    • React 使用 Object.is 比较算法 来比较 state。
  • React 会确保 dispatch 函数的标识是稳定的,并且不会在组件重新渲染时改变。

useCallback

const memoizedCallback = useCallback(
 () => {
   doSomething(a, b);
 },
 [a, b],
);

  • 返回一个 memoized 回调函数。

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

  • 该回调函数仅在某个依赖项改变时才会更新。

  • 用于当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件。

useMemo

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
  • 返回一个 memoized 值。

  • 把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。

  • 这种优化有助于避免在每次渲染时都进行高开销的计算。

  • 传入 useMemo 的函数会在渲染期间执行。

    • 请不要在这个函数内部执行与渲染无关的操作
    • 诸如副作用这类的操作属于 useEffect 的适用范畴,而不是 useMemo。
  • 如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。

useRef

const refContainer = useRef(initialValue);
  • useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。

  • 返回的 ref 对象在组件的整个生命周期内保持不变。

  • useRef() 比 ref 属性更有用。它可以很方便地保存任何可变值,其类似于在 class 中使用实例字段的方式。

  • useRef() 创建的是一个普通 Javascript 对象。和自建一个 {current: ...} 对象的唯一区别是,useRef 会在每次渲染时返回同一个 ref 对象。

  • 当 ref 对象内容发生变化时,useRef 并不会通知你。变更 .current 属性不会引发组件重新渲染。

  • 如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用回调 ref 来实现。

useImperativeHandle

useImperativeHandle(ref, createHandle, [deps])
  • useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。

  • 在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef 一起使用

useLayoutEffect

  • 其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect。
    • 可以使用它来读取 DOM 布局并同步触发重渲染。
  • 在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。
  • 尽可能使用标准的 useEffect 以避免阻塞视觉更新。
  • useLayoutEffect 与 componentDidMount、componentDidUpdate 的调用阶段是一样的。
    • 推荐你一开始先用 useEffect,只有当它出问题的时候再尝试使用 useLayoutEffect。
  • useLayoutEffect 还是 useEffect 都无法在 Javascript 代码加载完成之前执行。
    • 这就是为什么在服务端渲染组件中引入 useLayoutEffect 代码时会触发 React 告警。
    • 解决1:需要将代码逻辑移至 useEffect 中(如果首次渲染不需要这段逻辑的情况下),
    • 解决2:将该组件延迟到客户端渲染完成后再显示(如果直到 useLayoutEffect 执行之前 HTML 都显示错乱的情况下)。
    • 若要从服务端渲染的 HTML 中排除依赖布局 effect 的组件,可以通过使用 showChild && 进行条件渲染,并使用 useEffect(() => { setShowChild(true); }, []) 延迟展示组件。这样,在客户端渲染完成之前,UI 就不会像之前那样显示错乱了。

useDebugValue

useDebugValue(value)
  • useDebugValue 可用于在 React 开发者工具中显示自定义 hook 的标签。
  • 格式化值的显示可能是一项开销很大的操作。除非需要检查 Hook 的特殊情况
    • 接受一个格式化函数作为可选的第二个参数。
    • 该函数只有在 Hook 被检查时才会被调用。它接受 debug 值作为参数,并且会返回一个格式化的显示值。
  • 不推荐向每个自定义 Hook 添加 debug 值。当它作为共享库的一部分时才最有价值。

官方 Hooks 的 FAQ 整理

官方推荐使用 Hooks 成为编写 React 组件的主要方式。

Hook 对静态类型支持十分友好。

Hooks 的 lint 规则强制了两点规则:(ESLint 插件 ) - 对 Hook 的调用要么在一个大驼峰法命名的函数(视作一个组件)内部,要么在另一个 useSomething 函数(视作一个自定义 Hook)中。 - Hook 在每次渲染时都按照相同的顺序被调用。

Class 的生命周期方法对应到 Hook - constructor:函数组件不需要构造函数。你可以通过调用 useState 来初始化 state。如果计算的代价比较昂贵,你可以传一个函数给 useState。 - getDerivedStateFromProps:改为 在渲染时 安排一次更新。 - shouldComponentUpdate - render:这是函数组件体本身。 - componentDidMount, componentDidUpdate, componentWillUnmount:useEffect Hook 可以表达所有这些(包括 不那么 常见 的场景)的组合。 - getSnapshotBeforeUpdate,componentDidCatch 以及 getDerivedStateFromError:目前还没有这些方法的 Hook 等价写法,但很快会被添加。

类似实例变量:useRef() Hook 不仅可以用于 DOM refs。「ref」 对象是一个 current 属性可变且可以容纳任意值的通用容器,类似于一个 class 的实例属性。

在定义 state 变量时,推荐把 state 切分成多个 state 变量,每个变量包含的不同值会在同时发生变化。

通过 ref 来手动实现获取上一轮的 props 或 state。

若函数中看到陈旧的 props 和 state。 - 想要从某些异步回调中读取 最新的 state,你可以用 一个 ref 来保存它,修改它,并从中读取。 - 使用了「依赖数组」优化但没有正确地指定所有的依赖。

使用 useReducer 以一个增长的计数器来在 state 没变的时候依然强制一次重新渲染

获取 DOM 节点的位置或是大小的基本方式是使用 callback ref。每当 ref 被附加到一个另一个节点,React 就会调用 callback。

条件式的发起 effect 可以在更新时跳过 effect,忘记处理更新常会 导致 bug,这也正是我们没有默认使用条件式 effect 的原因。

避免在 effect 外部去声明所需要的函数,记住 effect 外部的函数使用了哪些 props 和 state 很难。 - 如果没用到组件作用域中的任何值,就可以安全地把条件参数指定为 []

在特殊需求下,可以使用一个 ref 来保存一个可变的变量,类似 class 中的 this 的功能。

用 React.memo 包裹一个组件来对它的 props 进行浅比较,实现 shouldComponentUpdate

使用 useMemo Hook 允许你通过「记住」上一次计算结果的方式在多次渲染的之间缓存计算结果。

Hook 不会因为在渲染时创建函数而变慢吗。在现代浏览器中,闭包和类的原始性能只有在极端场景下才会有明显的差别。

Hook 的设计在某些方面更加高效: - Hook 避免了 class 需要的额外开支,像是创建类实例和在构造函数中绑定事件处理器的成本。 - 符合语言习惯的代码在使用 Hook 时不需要很深的组件树嵌套。这个现象在使用高阶组件、render props、和 context 的代码库中非常普遍。组件树小了,React 的工作量也随之减少。

React 内联函数,每次渲染都传递新的回调会如何破坏子组件的 shouldComponentUpdate 优化有关。Hook 从三个方面解决了这个问题。 - useCallback Hook 允许你在重新渲染之间保持对相同的回调引用以使得shouldComponentUpdate 继续工作 - useMemo Hook 使得控制具体子节点何时更新变得更容易,减少了对纯组件的需要。

- useReducer Hook 减少了对深层传递回调的依赖,正如下面解释的那样。

若一个经常变化的值,则推荐 在 context 中向下传递 dispatch 而非在 props 中使用独立的回调。

Hook 只会在 React 组件中被调用(或自定义 Hook —— 同样只会在 React 组件中被调用)。

多个 useState() 调用会得到各自独立的本地 state 的原因。 - 每个组件内部都有一个「记忆单元格」列表。它们只不过是我们用来存储一些数据的 JavaScript 对象。 - 当你用 useState() 调用一个 Hook 的时候,它会读取当前的单元格(或在首次渲染时将其初始化),然后把指针移动到下一个。