探索React Hook

1,199 阅读11分钟

本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

hello,大家好,我是小憨憨,一个持续性学习,不间断写bug的前端工程师

前言

写过react的小伙伴都知道哈,在react16.8版本之前,组件分为class组件(有状态组件)和函数式组件(无状态组件)。

有状态组件主要用来定义交互逻辑和业务数据,接收props和维护内部的state数据,通常会伴随着一系列的副作用;

无状态组件是一个纯函数,所以它以参数的形式接收props并进行一系列操作,并且其内部没有state,最大的特点就是无副作用。

相信很多小伙伴在开发过程中都遇到过这样的情况:在需求初期开发了一个方便使用的函数式组件,随着后期需求的迭代结果发现此时需要在这个函数式组件内维护自身的state才能满足我们的新需求,但是我们都知道函数式组件是不支持内部维护state的,此时我们就不得不将函数式组件改写为class组件。为了解决这种问题,在react16.8版本之后官方提出了 Hook, 它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

Hook简介

首先react新特性Hook的添加,对现有的React没有破坏性改动,其次它是完全可选的(意味着你在开发中可用可不用),100%向后兼容现在可用(自16.8版本之后可用)。并且官方没有计划从React中移除class。最后,Hook并不会影响你对React概念的理解,恰恰相反,Hook 为已知的 React 概念提供了更直接的 API:props, state,context,refs 以及生命周期。

在我们以往的React开发中,都会有这样的问题:

  1. 在组件之间复用状态逻辑很难;
  2. 复杂组件变得难以理解;
  3. 难以理解的class。 而Hook可以很好的解决我们上述的问题:
  4. Hook 使你在无需修改组件结构的情况下复用状态逻辑。
  5. Hook 可以将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据)。
  6. Hook 使你在非 class 的情况下可以使用更多的 React 特性。 从概念上讲,React 组件一直更像是函数。而 Hook 则拥抱了函数,同时也没有牺牲 React 的精神原则。

Hook是什么?

Hook是一个特殊的函数,它可以让你“钩入”React的特性。比如我们使用useState可以在函数式组件中添加state。

什么时候会用Hook?

正如我们上面所说的,当你在编写函数式组件且需要向其内部添加一些state的时候,就可以使用Hook

Hook使用规则

Hook本质上是JavaScript函数,在使用时需要遵循一下规则:

  • 只在最顶层使用Hook,不要在循环,条件或者嵌套中使用hook,确保总是在你的 React 函数的最顶层以及任何 return 之前调用他们。遵守这条规则,就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的 useState 和 useEffect 调用之间保持 hook 状态的正确。
  • 只在React函数中调用Hook,不要在普通的javaScript函数中调用Hook
  • Hook 在 class 内部是不起作用的

Hook API概览

接下来我们来看看HooK相关的API

useState

import {useState} from 'react'
const [state, setState] = useState(initialState)

const [count, setCount] = useState(initialState)

useState方法,接收一个任意类型的可选参数,用来初始化数据。其返回值包括一个state以及用于更新state的函数。并且state中的变量会被React保留。 举个🌰:

function Example () {
    const [count, setCount] = useState(0)
    return (
        <div>
            <p>You clicked {count} times</p>
            <button onClick={() => setCount(count + 1)}>
                Click me
            </button>
        </div>
    )
}
  • useState方法返回的第一个值始终是更新后最新的state,下图是上述代码运行之后点击了三次之后的结果 image.png
  • 函数式更新:如果新的 state 需要通过使用先前的 state 计算得出,那么可以将函数传递给 setState。该函数将接收先前的 state,并返回一个更新后的值。看下面的🌰:
import React, {useState} from 'react';
import ReactDOM from 'react-dom';
import './index.css';


function Example () {
    const [count] = useState(0)
    return (
        <div>
            <Counter initialCount={count}></Counter>
        </div>
    )
}

function Counter({initialCount}) {
    const [count, setCount] = useState(initialCount);
    return (
      <>
        Count: {count}
        <button onClick={() => setCount(initialCount)}>Reset</button>
        <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
        <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
      </>
    );
  }
  
ReactDOM.render(
    <Example />,
    document.getElementById('root')
);
  

注意上述代码中三个button的点击事件,其中后两个setCount接收了一个函数作为参数。 PS: 与 class 组件中的 setState 方法不同,useState 不会自动合并更新对象。 那么,我们在需要合并的时候需要手动的使用展开运算符或者Object.assign进行合并。

  • 惰性初始state: initialState 参数只会在组件的初始渲染中起作用,后续渲染时会被忽略。如果初始 state 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用:
const [state, setState] = useState(() => {
  const initialState = someExpensiveComputation(props);
  return initialState;
});
  • 跳过state更新:调用 State Hook 的更新函数并传入当前的 state 时,React 将跳过子组件的渲染及 effect 的执行。

useEffect

  • useEffect做了什么? 使用useEffect你可以告诉React组件需要在渲染后执行某些操作。React会保存你传递的函数(我们将其称之为effect),并且在执行DOM更新之后调用它。

  • 为什么在组件内部调用 useEffect? 将useEffect放在组件内部可以让我们在effect直接访问到useState定义的state变量或者其它的props

import React, { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}
  • 该 Hook 接收一个包含命令式、且可能有副作用代码的函数。

  • 默认情况下,useEffect在第一次渲染之后和每次更新之后都会执行,但你可以选择让它在只有某些值改变的时候 才执行。 PS: 与 componentDidMount 或 componentDidUpdate 不同,使用 useEffect 调度的 effect 不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快。

  • 我们都知道在class组件中,需要执行一些副作用操作时,比如:发起ajax请求,添加事件监听和销毁,修改dom等,我们通常会把这些副作用函数写在生命周期钩子中,通常是componentDidMount,componentDidUpdate,componentWillUnmount。而useEffect是将这三个生命周期钩子三合一了。

  • 清除effect:通常,当我们需要清除副作用函数比如订阅或者定时器等时,调用useEffect方法需要返回一个清除函数。看🌰:

useEffect(() => {
  const subscription = props.source.subscribe();
  return () => {
    // 清除订阅
    subscription.unsubscribe();
  };
});

为防止内存泄漏,清除函数会在组件卸载前执行。如果组件多次渲染(通常如此),则在执行下一个 effect 之前,上一个 effect 就已被清除。在上述示例中,意味着组件的每一次更新都会创建新的订阅。

  • useEffect的执行时机:默认情况下,useEffect 会在浏览器绘制后延迟执行(相当于componentDidMount, componentDidUpdate),但会保证在任何新的渲染前执行。React 将在组件更新前刷新上一轮渲染的 effect。
  • useEffect的条件执行:useEffect可以接收第二个参数,它是effect所依赖的值数组。(这句话的意思就是说,useEffect的第二个参数是个数组,其数组内的值用来控制useEffect方法是否执行。[] 表示只在第一次执行,此时的效果相当于相当于componentDidMount[name] 表示只有当name改变后才会执行。)

useContext

接收一个context对象(React.createContext的返回值)并返回该context的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定。

当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。即使祖先使用 React.memo 或 shouldComponentUpdate,也会在组件本身使用 useContext 时重新渲染。

  • useContext 的参数必须是 context 对象本身
  • 调用了 useContext 的组件总会在 context 值变化时重新渲染。

useReducer

useReducer从字面意思上我们就可以看出类似于redux中的reducer。而在Hook中useReducer是useState的替代方案,它接收一个形如 (state, action) => newState 的reducer,并返回当前的state以及其配套的dispatch方法。

const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

useRef

类似于我们之前class组件和React元素中使用的React.createRef,在Hook中我们使用useRef。 useRef返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。

const refContainer = useRef(initialValue);

useRef 返回的 ref 对象在组件的整个生命周期内保持不变,也就是说每次重新渲染函数组件时,返回的ref 对象都是同一个(使用 React.createRef ,每次重新渲染组件都会重新创建 ref)

useCallback

把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。

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

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

useMemo

把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。

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

useMemo是一种性能优化手段,类似于我们class组件中的shouldComponentUpdate的生命周期钩子,和pureComponent组件

useImperativeHandle

useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef 一起使用:

function FancyInput(props, ref) {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);

useLayoutEffect

其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。

尽可能使用标准的 useEffect 以避免阻塞视觉更新。

useDebugValue

useDebugValue 可用于在 React 开发者工具中显示自定义 hook 的标签。

useDebugValue 接受一个格式化函数作为可选的第二个参数。该函数只有在 Hook 被检查时才会被调用。它接受 debug 值作为参数,并且会返回一个格式化的显示值。

useDebugValue(date, date => date.toDateString());

自定义Hook

自定义 Hook 是一个函数,其名称必须以 “use” 开头,函数内部可以调用其他的 Hook。

在两个组件中使用相同的 Hook 会共享 state 吗?

不会。自定义 Hook 是一种重用状态逻辑的机制(例如设置为订阅并存储当前值),所以每次使用自定义 Hook 时,其中的所有 state 和副作用都是完全隔离的。

自定义 Hook 如何获取独立的 state?

每次调用 Hook,它都会获取独立的 state。我们可以在一个组件中多次调用 useState 和 useEffect,它们是完全独立的。

从Class组件到HooK的迁移

生命周期方法如何对应到Hook?

  • constructor:函数组件不需要构造函数。你可以通过调用 useState 来初始化 state。如果计算的代价比较昂贵,你可以传一个函数给 useState。

  • getDerivedStateFromProps:改为 在渲染时 安排一次更新。

  • shouldComponentUpdate:对应到 React.memo。 React.memo等效于 PureComponent,但它只比较 props。你也可以通过第二个参数指定一个自定义的比较函数来比较新旧 props。如果函数返回 true,就会跳过更新。

React.memo 不比较 state,因为没有单一的 state 对象可供比较。但你也可以让子节点变为纯组件,或者 用 useMemo 优化每一个具体的子节点。

  • render:这是函数组件体本身。

  • componentDidMount, componentDidUpdate, componentWillUnmount:使用useEffect可以表达所有这些(包括不那么常见的场景)的组合。

  • getSnapshotBeforeUpdate,componentDidCatch 以及 getDerivedStateFromError:目前还没有这些方法的 Hook 等价写法,但很快会被添加。

参考

React Hook官方文档

推荐阅读

终于搞懂React Hook了

React Hook详解【近1w字】+项目实战

你真的了解 React 生命周期吗

写在最后

写作整理不易,欢迎点(一)赞(键)收(三)藏(连),若有错误的地方,欢迎评论指正!!!