宝啊~来聊聊 9 种 React Hook

2022-02-28 09:40:17 浏览数 (1)

Hook

精通 React Hook ,看这一篇足矣了。

文章会为你讲述 React 9种 Hook 的日常用法以及进阶操作,从浅入深彻底掌握 React Hook!

useState

useState 的用法如果不太了解的小伙伴可以移步 React 中文文档,它和 Class Component 中的 this.setState 类似。

当我们使用 useState 定义 state 变量时候,它返回一个有两个值的数组。第一个值是当前的 state,第二个值是更新 state 的函数。

所谓 State 产生的闭包

关于 useState 产生的闭包,我们来看这样一段代码:

代码语言:javascript复制
import React, { useState } from 'react'

const DemoState: React.FC = () => {
  const [count, setCount] = useState(0)

  const handleClickBtn = () => {
    setCount(count   1)
  }

  const handleAlert = () => {
    setTimeout(() => {
      alert(count)
    }, 3000)
  }

  return <div>
    <div style={{ margin: '20px' }}>
      <button onClick={handleClickBtn}>Click Me !</button>
    </div>
    <p onClick={handleAlert}>This is Count: {count}</p>
  </div>
}

export {
  DemoState
}

页面上会渲染:

  • 一个 Button , 当我们点击 Button 时 count 的值会递增加一。
  • 一个 p 标签 , 当我们点击 p 标签时定时器会在 3s 后打印出 count 的值。

接下来让我们进行这样的操作:

点击 P 标签,快速点击三次 Click Me!之后。此时 count 的值在页面上已经更新为 3 ,但是在 3s 后的 setTimeout 中打印仍然会是0。

其实当 DemoState 函数每次运行我们都称他为每一次渲染,每一次渲染函数内部都拥有自己独立的 props 和 state,当在 jsx 中调用代码中的 state 进行渲染时,每一次渲染都会获得各自渲染作用域内的 props 和 state 。

所以当定时器触发时,拿的的 count 因为闭包原因是 DemoState 函数第一次渲染时内部的 count 值,alert 的结果为0也就不足为奇了。

如果你对这里的基础内容仍然还是存在不解,可以移步细说React中的useRef。

所谓批量更新原则

熟悉 React 的同学都清楚所谓 state 的变化 React 内部遵循的是批量更新原则。

所谓异步批量是指在一次页面更新中如果涉及多次 state 修改时,会合并多次 state 修改的结果得到最终结果从而进行一次页面更新。

关于批量更新原则也仅仅在合成事件中通过开启 isBatchUpdating 状态才会开启批量更新,简单来说"

  1. 凡是**React**可以管控的地方,他就是异步批量更新。比如事件函数,生命周期函数中,组件内部同步代码。
  2. 凡是**React**不能管控的地方,就是同步批量更新。比如setTimeout,setInterval,源生DOM事件中,包括**Promise**中都是同步批量更新。

在 React 18 中通过 createRoot 中对外部事件处理程序进行批量处理,换句话说最新的 React 中关于 setTimeout、setInterval 等不能管控的地方都变为了批量更新。 关于合成事件与批量更新,你可以移步深入挖掘 React 中的 state这篇文章。

实现 useSetState

在 Class Component 中的 this.setState 中支持传入合并传入 setState 参数,在 useState中如果传入 object 类型会发生覆盖。

假使我们需要在 useState 中实现这个需求,我们可以通过额外封装一个 useSetState Hook 去实现:

代码语言:javascript复制
import { useCallback, useState } from 'react';

function useSetState<T extends {}>(
  initialState: T = {} as T
): [T, (patch: Partial<T> | ((prevState: T) => Partial<T>)) => void] {
  const [state, set] = useState<T>(initialState);

  const setState = useCallback((patch) => {
    set((preState) =>
      Object.assign(
        {},
        preState,
        typeof patch === 'function' ? patch(preState) : patch
      )
    );
  }, []);

  return [state, setState];
}

export default useSetState;

你可以点击这个 CodeSanBox 地址查看具体实例。

useEffect

useEffect 被称为副作用钩子,这个 Hook 和 useState 一样是一个基础钩子。Effect Hook 可以让你在函数组件中执行副作用操作。

useEffect Hook 支持两个参数,第一个参数为一个函数表示副作用效应函数,默认情况下它在第一次渲染之后和每次更新之后都会执行。

第二个参数是一个数组,指定了第一个参数(副效应函数)的依赖项。只有该数组中的变量发生变化时,副效应函数才会执行。

关于 useEffect 这个 Hook ,更多基础用法你可以查阅React 官方文档,文档中关于 useEffect 的内容还是比较全面的,我就不累赘了。

实现 useUpdateEffect

在 Class 组件中存在 componentDidUpdate 生命周期。它会在更新后会被立即调用,首次渲染不会执行此方法。

在 Function Component 中我们可以借助 useEffect 额外封装实现 componentDidUpdate 的功能:

首先我们可以通过 useRef 实现一个判断是否是首次渲染的 Hook:

代码语言:javascript复制
import { useRef } from 'react';

export function useFirstMountState(): boolean {
  const isFirst = useRef(true);

  if (isFirst.current) {
    isFirst.current = false;

    return true;
  }

  return isFirst.current;
}

如果你还不是特别了解 useRef 的作用,没关系我会带你在后边详细了解它的机制。这里你仅需要了解这个 useFirstMountState 这个 hook 会在组件首次渲染时返回 true , 其余时候返回 false。

接下来就很简单了,借助 useFirstMountState 我们可以判断是否是页面首次渲染。那么仅需要在 Effect 中判断是否是首次更新即可:

代码语言:javascript复制
import { useEffect } from 'react';
import { useFirstMountState } from './useFirstMountState';

const useUpdateEffect: typeof useEffect = (effect, deps) => {
  const isFirstMount = useFirstMountState();

  useEffect(() => {
    if (!isFirstMount) {
      return effect();
    }
  }, deps);
};

export default useUpdateEffect;

useContext

Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props。

熟悉 React 中Context Api 和 Vue 中的 provide/inject Api 的同学可能会对这个钩子的作用深有体会。

假设这样一种场景:

在根级别组件上我们需要向下传递一个用户名 username 的属性给每一个子组件进行使用。

此时,如果使用 props 的方法进行层层传递那么无疑是一种噩梦。而且如果我们的 G 组件需要使用 username 但是 B、E 并不需要,如果使用 props 的方法难免在 B、E 组件内部也要显式声明 username。

React 中正是为了解决这样的场景提出来 Context Api。

可以通过 React.createContext 创建 context 对象,在跟组件中通过 Context.Provider 的 value 属性进行传递 username ,从而在 Function Component 中使用 useContext(Context) 获取对应的值。

useContext(MyContext) 只是让你能够读取 context 的值以及订阅 context 的变化。你仍然需要在上层组件树中使用 <MyContext.Provider> 来为下层组件提供 context。 关于 Context && useContext 详细的用法可以查看这里,具体 API 在官网中已经给予了非常全面的说明。

useReducer

上边我们提到过基础的状态管理钩子 useState ,在 React Hook 中额外提供了一个关于状态管理的 useReducer。

useReducer 用法

const [state, dispatch] = useReducer(reducer, initialArg, init);

useReducer 接受三个参数分别是 reducer 函数、初始值 initialArg 以及一个可选的惰性初始化的 init 函数。

它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。

让我们通过一个简单的计数器例子来了解一下它的基础用法:

代码语言:javascript复制
import { useReducer } from 'react';

interface IState {
  count: number;
}

interface IAction {
  type: 'add' | 'subtract';
  payload: number;
}

const initialState: IState = { count: 0 };

const reducer = (state: IState, action: IAction) => {
  switch (action.type) {
    case 'add':
      return { count: state.count   action.payload };
    case 'subtract':
      return { count: state.count - action.payload };
    default:
      throw new Error('Illegal operation in reducer.');
  }
};

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      <h1>Hello , My name is 19Qingfeng.</h1>
      <p>Counter: {state.count}</p>
      <p>
        <button onClick={() => dispatch({ type: 'add', payload: 1 })}>
          add 1!
        </button>
      </p>
      <p>
        <button onClick={() => dispatch({ type: 'subtract', payload: 1 })}>
          subtract 1!
        </button>
      </p>
    </>
  );
}

export default Counter;

这里我们创建了一个简单的 Counter 计数器组件,内部通过 useReducer 管理 couter 的状态。

你可以在这里来体验这个计数器小例子。

useState & useReducer

上边的计数器小例子我们其实通过 setState 完全也可以实现,大部分同学在写 component 时应该有存在这样一个疑问:

「什么时候使用 useState 又什么时候使用 useReducer ,useReducer 相比 useState 存在什么优势/不足呢?」

其实在日常大多数情况下使用 useState 完全可以满足日常开发的作用,毕竟如果对于一个简单的操作如果使用 action -> reducer -> store 这种方式去管理状态实在是有点大材小用。

关于状态管理究竟是使用 useState 还是 useReducer 绝大多数文章会告诉你 useReducer 适用于复杂的状态逻辑。

没错,日常应用中我我也是这样使用的,存在多种复杂状态管理时利用 reducer 函数根据不同 action 去派发状态更新。

但是话又说回来如果某个 state 下存在很多操作状态,每个操作都有很多逻辑,对于这样复杂的状态,使用 useState 拥有单独的功能管理相比 reducer 中单个函数中的多个不同动作也许会更加清晰一些。

关于「什么时候使用 useState 又什么时候使用 useReducer」,在我个人看来这两种方式的使用更像是一种取舍总而言之尽量使用你觉得舒服的方法,对你和同事来说更容易理解就可以了。

深更新的组件做性能优化

在 useReducer 的官方文档中存在这样一句介绍:

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

在某些场景下我们通常会将函数作为 props 传递到 child component 中去,这样的话,每次父组件 re-render 时即使我们并没有修改当作 props 的函数,子组件也会重新渲染。

我们来一起看一下这个例子:

代码语言:javascript复制
// 父组件
import { useState } from 'react';
import ChildComponent from './Child';

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

  const callback = () => {
    return 10;
  };

  return (
    <div>
      <h3>Hello This is Parent Component!</h3>
      <p>ParentCount: {count}</p>
      <button onClick={() => setCount(count   1)}>Click Me!</button>
      <br />
      <ChildComponent callback={callback} />
    </div>
  );
}

export default ParentComponent;
代码语言:javascript复制
// 子组件
import React, { FC, useEffect } from 'react';

interface Props {
  callback?: () => number;
}

const ChildComponent: FC<Props> = ({ callback }) => {
  useEffect(() => {
    alert('child re-render');
  }, [callback]);

  return (
    <>
      <h1>Hello This is Child Component</h1>
      <p>{callback && callback()}</p>
    </>
  );
};

export default ChildComponent;

这里我们在父组件中传递给子组件一个 callback 函数作为 props ,当我们点击页面上的按钮来看看会发生什么:

每次点击父组件的 button 时,子组件中的 effect 中被执行了。

此时其实我们传入子组件的 callback 并没有做什么改变,我们自然期望子组件中的 Effect 不会执行。

产生这个原因的机制是 React 每次渲染都会重新执行组件函数,当重新执行父组件时会重新生成一个 callback 函数。因为 React 内部使用 Object.is 判断,所以 React 会认为子组件的 props 发生了变化。 你可以点击这里查看 CodeSanBox 例子

而在 useReduce 中返回的 dispatch 正是一个函数,但是 useReducer 的好处之一便是, dispatch 不会随着 re-render 而重新分配记忆位置,比方上述我们将 dispatch 作为 props 传入 child component 中时子组件中的 Effect 也并不会被执行。

有兴趣的同学可以私下自己去尝试下,当然使用 useCallback 包括我们上述 Demo 中父组件的函数也是可以达到相同的效果,但是如此也就意味着說我们有非常多的 callback 需要绑在 useCallback 里边,这也许并不是一件好事。

useCallback

接下来我们来聊一聊 useCallback ,它的最大作用体现在 React 中的性能优化。

老样子,我们先来看看基础用法:

代码语言:javascript复制
const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

useCallback 接受两个参数:

  • 第一个参数是一个函数,这个函数仅会在对应依赖项发生变化之后才会被重新生成,或者说这个函数被产生「记忆」。
  • 第二个参数是一个数组,它表示第一个参数所依赖的依赖项,仅在该数组中某一项发生变化时第一个参数的函数才会「清除记忆」重新生成。

也许大多数接触 React 的朋友会好奇这个 Hook 的使用场景,此时让我们来回忆一下在 useReducer 章节讲到的例子。

我们在父组件中传递了一个 callback 函数作为 props 传递给了子组件,每次渲染中我们并没有改变 callback 但是每次父组件 re-render ,React 仍然会认为 callback 发生变化从而造成多余的子组件 re-render 。

如果忘记了这个例子的朋友可以翻到 useReducer 环节重新温习一下。

此时,使用 useCallback 就可以很好的解决这个例子。

让我们稍微来改造一下上边父组件中的代码:

代码语言:javascript复制
import { useCallback, useState } from 'react';
import ChildComponent from './Child';

function ParentComponent() {
  const [count, setCount] = useState(0);
    
  // 这里我们使用了 useCallback 进行包裹
  const callback = useCallback(() => {
    return 10;
  }, []);

  return (
    <div>
      <h3>Hello This is Parent Component!</h3>
      <p>ParentCount: {count}</p>
      <button onClick={() => setCount(count   1)}>Click Me!</button>
      <br />
      <ChildComponent callback={callback} />
    </div>
  );
}

export default ParentComponent;

可以看到我们使用 useCallback 包裹了传入子组件的回调函数,同时第二个依赖项参数传递一个空数组。

此时我们来看看页面的展示效果:

此时即使我们多次点击按钮,子组件的 Effect 也并不会执行了。

你可以点击这里查看 CodeSanBox。

useMemo

useMemo 作用

如果说 useCallback 是 React 团队提供给开发者作为对于函数的优化手段,那么 useMemo 就可以看作用于「记忆」值从而带来性能优化。

代码语言:javascript复制
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

useMemo 同样是作为性能优化提供的 Hook ,它相比 useCallback 来说支持任意类型的值都可以被记忆。

同样它支持两个参数:

  • 第一参数接受传入一个函数,传入的函数调用返回值会被「记忆」。仅仅当依赖项发生变化时,传入的函数才会重新执行计算新的返回结果。
  • 第二个参数同样也是一个数组,它表示第一个参数对应的依赖项。

我们来看这样一个例子:

代码语言:javascript复制
import React, { useState } from 'react';

const data = [
  { name: '数学', id: 0 },
  { name: '语文', id: 1 },
  { name: '英语', id: 2 },
  { name: '化学', id: 3 },
  { name: '生物', id: 4 },
];

function Demo(): React.ReactElement {
  const [count, setCount] = useState(0);
  const renderSubject = (() => {
    console.log('Recalculate renderSubject !');
    return data.map((i) => <li key={i.id}>{i.name}</li>);
  })();
  return (
    <div>
      <p>count: {count}</p>
      <button onClick={() => setCount(count   1)}>Click Me !</button>
      <br />
      <h1>科目:</h1>
      <ul>{renderSubject}</ul>
    </div>
  );
}

export default Demo;

当我们每次点击 button 组件 re-render 时,renderSubject 的值都会重新计算也就是说每次都会打印出 Recalculate renderSubject !

此时让我们在换成 useMemo 包裹 renderSubject ,告诉 React 「记忆」 renderSubject 的值再重新试一试:

代码语言:javascript复制
import React, { useMemo, useState } from 'react';

const data = [
  { name: '数学', id: 0 },
  { name: '语文', id: 1 },
  { name: '英语', id: 2 },
  { name: '化学', id: 3 },
  { name: '生物', id: 4 },
];

function Demo(): React.ReactElement {
  const [count, setCount] = useState(0);
  const renderSubject = useMemo(() => {
    return (() => {
      console.log('Recalculate renderSubject !');
      return data.map((i) => <li key={i.id}>{i.name}</li>);
    })();
  }, []);

  return (
    <div>
      <p>count: {count}</p>
      <button onClick={() => setCount(count   1)}>Click Me !</button>
      <br />
      <h1>科目:</h1>
      <ul>{renderSubject}</ul>
    </div>
  );
}

export default Demo;

此时当我们点击页面上的 button 时,count 发生变化页面 re-render 时,因为我们使用 useMemo 传入的函数中返回 data.map((i) => <li key={i.id}>{i.name}</li>) 并且第二个参数是一个空数组。

无论页面如何 re-render ,只要依赖项不发生变化那么 useMemo 中返回的值就不会重新计算。

文章中的示例代码为了展示 Hook 的作用故意设计如此的,这里大家理解需要表达的意义即可。

关于性能优化我想说的事

关于 useCallback 以及 useMemo 这两个 Hook 都是 React 提供给开发者作为性能优化手段的方法。

但是大多数时候,你不需要考虑去优化不必要的重新渲染。 React 是非常快的,我能想到你可以利用时间去做很多事情,比起做这些类似的优化要好得多。

对于 useCallback 和 useMemo 来说,我个人认为不合理的利用这两个 Hook 不仅仅会使代码更加复杂,同时有可能会通过调用内置的 Hook 防止依赖项和 memoized 的值被垃圾回收从而导致性能变差。

如果说,有些情况下比如交互特别复杂的图表、动画之类,使用这两个 Hook 可以使你获得了必要的性能收益,那么这些成本都是值得承担的,但最好使用之前先测量一下

官方文档指出,无需担心创建函数会导致性能问题。我们上述提供的例子仅仅是为了向大家展示它们的用法,实际场景下非常不建议这样使用。 关于 useCallback 、 useMemo 的误区用法,你可以查看这篇文章useCallback/useMemo 的使用误区

useRef

useRef Hook 的作用主要有两个:

  • 多次渲染之间保证唯一值的纽带。

useRef 会在所有的 render 中保持对返回值的唯一引用。因为所有对ref的赋值和取值拿到的都是最终的状态,并不会因为不同的 render 中存在不同的隔离。

这点我们在开头的 useEffect Hook 中就已经展示了它的示例,判断是否是由于页面更新而非首次渲染:

代码语言:javascript复制
import { useRef } from 'react';
export function useFirstMountState(): boolean {
  const isFirst = useRef(true);
  if (isFirst.current) {
    isFirst.current = false;
    return true;
  }
  return isFirst.current;
}
  • 获取 Dom 元素,在 Function Component 中我们可以通过 useRef 来获取对应的 Dom 元素。

关于 useRef 的作用和用法,我在这篇[细说 Reac t中的 useRef] 做了详尽说明。,你可以点击链接查看。

useImperativeHandle

useImperativeHandle 这个 Hook 很多同学日常可能用的不是很多,但是在某些情况下它会帮助我们实现一些意向不到的效果。

代码语言:javascript复制
useImperativeHandle(ref, createHandle, [deps])
  • ref 表示需要被赋值的 ref 对象。
  • createHandle 函数的返回值作为 ref.current 的值。
  • deps 依赖数组,依赖发生变化会重新执行 createHandle 函数。

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

我们来看这样一个例子:

代码语言:javascript复制
import React, {
  ForwardRefRenderFunction,
  useImperativeHandle,
  forwardRef,
  useRef,
} from 'react';

interface Props {
  name: string;
}

const Input: ForwardRefRenderFunction<{ focus: () => void }, Props> = (
  props,
  ref
) => {
  const inputRef = useRef<HTMLInputElement>(null);
  useImperativeHandle(
    ref,
    () => ({
      focus: () => inputRef.current?.focus(),
    }),
    []
  );

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

const ExportInput = forwardRef(Input);

export default ExportInput;

在上边的例子中,我们利用 useImperativeHandle 函数包括了外部传入的 ref 对象。

我们规定当外部通过 ref 获取该组件实例时,仅向外暴露出了个一个 focus 方法。

这正好对应了我们上边所提到的通过 useImperativeHandle 让你在使用 ref 时自定义暴露给父组件的实例值。

当然,在日常 React 开发中可能会存在这样一种情况。我们希望在父组件中调用子组件的方法,虽然 React 官方并不推荐这样声明式的写法,但是有时候我们不得不这样做。

我们稍微来改写一些上边的例子:

代码语言:javascript复制
import React, {
  ForwardRefRenderFunction,
  useImperativeHandle,
  forwardRef,
  useRef,
} from 'react';

interface Props {
  name: string;
}

export interface InputExportMethod {
  focus: () => void;
  domeSomeThing: () => void;
}

const Input: ForwardRefRenderFunction<InputExportMethod, Props> = (
  props,
  ref
) => {
  const inputRef = useRef<HTMLInputElement>(null);

  // 子组件方法
  const domeSomeThing = () => {
    // dosomething
    console.log('do smething');
  };

  useImperativeHandle(
    ref,
    () => ({
      focus: () => inputRef.current?.focus(),
      domeSomeThing: () => domeSomeThing(),
    }),
    []
  );

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

const ExportInput = forwardRef(Input);

export default ExportInput;

此时我可以在使用 Input 的父组件中通过 ref 调用到子组件通过 useImperativeHandle 暴露出的方法:

代码语言:javascript复制
import React, { useEffect, useRef } from 'react';
import Input, { InputExportMethod } from './index';

const Parent: React.FC = () => {
  const inputRef = useRef<InputExportMethod>(null);

  useEffect(() => {
    if (inputRef.current) {
      console.log(inputRef.current.domeSomeThing());
    }
  }, []);

  return <Input ref={inputRef} name="19Qingfeng"></Input>;
};

export default Parent;

此时当我们打开页面就会发现控制台成功的打印出:

useLayoutEffect

useLayoutEffect 与 useEffect 使用方式是完全一致的,useLayoutEffect 的区别在于它会在所有的 DOM 变更之后同步调用 effect。

可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前, useLayoutEffect 内部的更新计划将被同步刷新。

通常对于一些通过 JS 计算的布局,如果你想减少 useEffect 带来的「页面抖动」,你可以考虑使用 useLayoutEffect 来代替它。

来看看这样一段代码:

代码语言:javascript复制
import React, { useEffect, useRef, useState } from 'react';

function Demo() {
  const ref = useRef<HTMLDivElement>(null);
  const [style, setStyle] = useState<React.CSSProperties>({
    position: 'absolute',
    top: '200px',
    background: 'blue',
  });
  useEffect(() => {
    for (let i = 0; i < 1000; i  ) {
      console.log(i);
    }
    if (ref.current) {
      const { width, height, top, left } = ref.current.getBoundingClientRect();
      setStyle({
        width: width   'px',
        height: height   'px',
        top: top   'px',
        left: left   'px',
      });
    }
  }, []);
  return (
    <div>
      <div
        ref={ref}
        style={{ width: '200px', height: '200px', margin: '30px' }}
      >
        Hello
      </div>
      <div style={{ ...style, position: 'absolute' }}> This is 19Qingfeng.</div>
    </div>
  );
}

export default Demo;

这里我们使用了 useEffect 在页面刷新后重新计算了 This is 19Qingfeng. 这个 div 的位置。

大多数情况下刷新浏览器你可能得不到任何感知,让我们来降低浏览器 CPU 速率试试再来试试。

这里我在 chrome performance 下降低了 CPU 速率,此时让我们再来看看效果:

上边的 gif 中明显可以看到页面最初有一个蓝色 div 在跳动,而且通过 performance 分析明显可以看到使用 useEffect 页面出现了闪烁:

如果我们将 useEffect 更换称为 useLayoutEffect ,那么页面 useLayoutEffect 中的内容会在页面渲染前进行同步更新,有兴趣的同学可以私下自己验证一下。

当然 React 中所有的 Hook 都是 JS 脚本计算,如果你曾经碰到过在 Hook 中获取到不正确的页面元素位置时,或许这篇一次useEffect引发浏览器执行机制的思考会帮你解惑。

当然 useLayoutEffect 的使用还存在一些特殊情况:

有时你可能需要使用另外一个情况下使用 useLayoutEffect ,而不是  useEffect ,如果你要更新的值(像 ref ),此时你需要确保它是在最新的任何其他代码运行之前。例如:

代码语言:javascript复制
const ref = React.useRef()
React.useEffect(() => {
  ref.current = 'some value'
})

// then, later in another hook or something
React.useLayoutEffect(() => {
  console.log(ref.current) // <-- this logs an old value because this runs first!
})

在这种特殊情况下,使用 useLayoutEffect 是一个非常不错的解决方案。

本质上还是 useLayoutEffect 的实现是基于 micro ,而 Effect 是基于 macro ,所以 useLayoutEffect 会在页面更新前去执行。

useDebugValue

代码语言:javascript复制
useDebugValue(value , fn)

useDebugValue  可用于在 React 开发者工具中显示自定义 hook 的标签,它接受两个参数:

  • value 为我们要重点关注的变量,该参数表示在 DevTools 中显示的 hook 标志。
  • fn 表明如何格式化变量 value , 该函数只有在 Hook 被检查时才会被调用。它接受 debug 值作为参数,并且会返回一个格式化的显示值。

当我们自定义一些 Hook 时,可以通过 useDebugValue 配合 React DevTools 快速定位我们自己定义的 Hook。

代码语言:javascript复制
import { useDebugValue, useState } from 'react';

function useName() {
  const [state] = useState('19Qingfeng');
  useDebugValue('19Qingfeng');
  return state;
}

function App() {
  const name = useName();
  return <div>{name}</div>;
}

这段代码中我通过 useDebug 定义了一个 19Qingfeng 的标示,此时我们来查看一下 React DevTools :

需要额外注意的是:

  • useDebugValue应该在自定义hook中使用,如果直接在组件内使用是无效的。
  • 大部分情况下你不需要使用这个 Hook ,除非你在编写一些公共库的 Hook 时,显式标志该 Hook 。 2、如果使用useDebugValue,最好设置第2个参数,用于每次检查时格式化第一个参数。

结尾

感谢每一位可以读到结尾的朋友,希望文章中的内容可以帮助到你。

在 Webpack 专栏完结后,后续我会在专栏 React 中从零开始实现这 9 种 Hook,有兴趣的朋友可以关注我的React 专栏。

0 人点赞