• Hooks 只能在组件的顶层或自定义的 Hook 中调用它,不能在循环或条件内调用
    • 原因:Hooks 在 fiber 节点中以链表存储,如果写到循环或条件内则可能导致上一次渲染和这次的链表元素不能一一对应
    • 如果需要,则提取一个新组件并将 Hooks 移入其中
  • Hooks 只能在 Function Component 或自定义 Hook 中调用
    • 原因:fiber 树遍历时,会有全局变量存储当前 fiber 节点的引用,这样在调用 Hooks 函数时就知道当前 fiber 节点是哪个
    • 实现原理:在不同环境,Hooks 指向不同的函数,不在 Function Component 环境时指向的函数体都是抛错
  • 在严格模式下,React 将调用你回调函数两次,以帮助你发现意外的杂质。这只是开发行为,不会影响生产。如果回调函数是纯函数(应该如此),则这不应影响行为,其中一个调用的结果将被忽略

State Hooks

在组件保存一些用于渲染的信息,更新状态会重新渲染组件,并更新 UI。即:UI = f(state)

注意:

  • set 函数仅更新下一次渲染的状态变量。如果在调用 set 函数后读取状态变量,仍然会获得调用之前屏幕上的旧值
  • 如果提供的新值与当前 state 相同(通过 Object.is 比较确定),React 将跳过重新渲染组件及其子组件,这是一个优化
  • React 批量状态更新 会在所有事件处理程序运行并调用其 set 函数后更新屏幕。这可以防止在单个事件期间多次重新渲染。在极少数情况下,需要强制 React 提前更新屏幕,例如访问 DOM,可以使用 flushSync

useState

声明一个可以直接更新的状态变量

const [state, setState] = useState(initialState)
  • initialState:状态初始值,初始渲染后该参数将被忽略
    • 函数:调用函数(只在初始化组件时调用一次),并将返回值作为初始值
    • 其他值:直接作为初始值
  • state:当前的状态
  • setState:函数,更新状态并触发重新渲染
    • 传入参数为函数:视为更新函数执行,会将上一次状态作为唯一参数传入,返回值作为新状态
    • 传入参数不为函数:将传入参数作为新状态

useReducer

声明一个带有更新逻辑的函数的状态变量

const [state, dispatch] = useReducer(reducer, initialArg, init?)
  • reducer:指定状态如何更新的纯函数,以状态(state)和操作(action)作为参数,并且应该返回下一个状态。状态和动作可以是任何类型
  • initialArg:状态初始值,初始渲染后该参数将被忽略
    • 传了 init:把 init(initialArg) 的返回值作为初始值
    • 没传 initinitialArg 直接作为初始值
  • dispatch:函数,更新状态并触发重新渲染
    • 传入的参数作为 reducer 函数的 action 参数

示例:

import { useReducer } from 'react';
 
function reducer(state, action) {
  if (action.type === 'incremented_age') {
    return {
      age: state.age + 1
    };
  }
  throw Error('Unknown action.');
}
 
export default function Counter() {
  const [state, dispatch] = useReducer(reducer, { age: 42 });
 
  return (
    <>
      <button onClick={() => {
        dispatch({ type: 'incremented_age' })
      }}>
        Increment age
      </button>
      <p>Hello! You are {state.age}.</p>
    </>
  );
}

Context Hooks

上下文允许组件从远程父级接收信息,而无需将其作为 props 传递

useContext

读取并订阅上下文

const value = useContext(SomeContext)
  • SomeContext:之前使用 createContext 创建的上下文。上下文本身不保存信息,它仅表示可以提供或从组件中读取的信息类型
  • value:最近的 SomeContext.Provider 的上下文值,如果没有上层 SomeContext.Provider 则是 createContext 时传入的初始值
    • 返回的值始终是最新的。如果上下文发生变化,React 会自动重新渲染读取某些上下文的组件

示例:

import { createContext, useContext } from 'react';
 
const ThemeContext = createContext(null); // 初始值为 null
 
export default function MyApp() {
  return (
    <ThemeContext.Provider value="dark">
      <Form />
    </ThemeContext.Provider>
  )
}
 
function Form() {
  return (
    <Panel title="Welcome">
      <Button>Sign up</Button>
      <Button>Log in</Button>
    </Panel>
  );
}
 
function Panel({ title, children }) {
  const theme = useContext(ThemeContext);
  const className = 'panel-' + theme;
  return (
    <section className={className}>
      <h1>{title}</h1>
      {children}
    </section>
  )
}

注意:

  • React 会自动重新渲染所有使用特定上下文的子级,从接收到不同 value 提供者开始。前一个值和后一个值通过 Object.is 进行比较。使用 memo 跳过重新渲染并不会阻止子级接收新的上下文值

Ref Hooks

Refs 让组件保存一些不用于渲染的信息,例如 DOM 节点或超时 ID。与状态不同,更新引用不会重新渲染的组件。 Refs 是 React 范式中的一个“逃生舱口”。当需要使用非 React 系统(例如内置浏览器 API)时,它们非常有用

useRef

声明一个引用。可以在其中保存任何值,最常见的是 DOM 节点

const ref = useRef(initialValue)
  • initialValue:初始值,可以是任意类型,初始渲染后该参数将被忽略
  • ref:对象,只有一个 current 属性,其值是当前值(如果没有修改,在下一次渲染将返回相同的值)
    • 如果将 ref 对象作为 JSX 节点的 ref 属性传递给 React,React 将设置其 current 属性为对应 DOM 节点

注意:

  • 渲染期间请勿写入或读取 ref.current,因为React 希望组件的主体表现得像一个纯函数
    • 如果输入(props state、context)相同,它应该返回完全相同的 JSX
    • 以不同的顺序或使用不同的参数调用它不应影响其他调用的结果
    • 而在渲染期间读取或写入 ref 打破了这些期望

通过使用 ref,可以确保:

  • 可以在重新渲染之间存储信息(与常规变量不同,常规变量在每次渲染时都会重置)
  • 该信息对于组件的每个副本来说都是本地的(与外部的变量不同,它们是共享的)
  • 更改它不会触发重新渲染(与触发重新渲染的状态变量不同)

useImperativeHandle

在组件的顶层调用 useImperativeHandle 来自定义它公开的 ref 句柄

useImperativeHandle(ref, createHandle, dependencies?)
  • ref:从 forwardRef 渲染函数中作为第二个参数收到的 ref
  • createHandle:一个不带参数并返回要公开的引用句柄的函数。该引用句柄可以有任何类型。通常将返回带有想要公开的方法的对象
  • dependenciescreateHandle 代码内部引用的所有反应值的列表(反应性值包括 props、state 以及直接在组件体内声明的所有变量和函数)
    • 不传:每次渲染都会调用
    • []:只在初始化渲染调用一次
    • [a, b, ...]:使用 Object.is 比较将每个依赖项与其之前的值进行比较,有一个不同就调用

示例:

import { forwardRef, useImperativeHandle } from 'react';
 
const MyInput = forwardRef(function MyInput(props, ref) {
  useImperativeHandle(ref, () => {
    return {
      a: 1,
      func() {},
    };
  }, []);
}

注意:

  • 不要过度使用,应该仅将 refs 用于无法表达为 props 的命令式行为:例如,滚动到节点、聚焦节点、触发动画、选择文本等。如果可以将某些内容表达为 prop,则不应使用 ref

Effect Hooks

Effect(副作用)允许组件连接到外部系统并与其同步。这包括处理网络、浏览器 DOM、动画、使用不同 UI 库编写的小部件以及其他非 React 代码

注意:

  • 不要过度使用,不尝试与某些外部系统同步则不需要 Effect

useEffect

useEffect(setup, dependencies?)
  • setup:具有副作用逻辑的函数,该函数可以选择返回清理函数。每次调用会先运行上次的清理函数,再运行本次的副作用函数
  • dependencies:决定 setup 调用时机的依赖项列表

触发时机:

  • 如果 Effect 不是由交互(如点击)引起的,React 通常会让浏览器在运行 Effect 之前先绘制更新的屏幕。如果 Effect 正在执行一些视觉操作(例如,定位工具提示),并且延迟很明显(例如,闪烁)则需要替换为 useLayoutEffect
  • 如果 Effect 是由交互(如点击)引起的, React 可能会在浏览器绘制更新的屏幕之前运行 Effect。这保证了 Effect 的结果可以被事件系统观察到。通常,这会按预期工作。但是,如果必须将工作推迟到绘制之后(如 alert),则可以使用 setTimeout。有关更多信息,请参阅 reactwg/react-18/128
  • 即使 Effect 是由交互(如单击)引起的, React 也可能允许浏览器在处理 Effect 内的状态更新之前重新绘制屏幕。通常,这会按预期工作。但是,如果必须阻止浏览器重新绘制屏幕,​​则需要替换为 useLayoutEffect

useLayoutEffect

useLayoutEffect(setup, dependencies?)

触发时机:在浏览器重新绘制屏幕之前触发,可以在此处测量布局

注意:

  • useLayoutEffect 中的代码以及从中安排的所有状态更新都会阻止浏览器重新绘制屏幕。过度使用会使应用程序变慢。如果可能,首选 useEffect
  • 如果在 useLayoutEffect 内触发状态更新,React 将立即执行所有剩余的 Effects,包括 useEffect

useInsertionEffect

允许在任何布局效果触发之前将元素插入到 DOM 中

useInsertionEffect(setup, dependencies?)

触发时机:在 React 对 DOM 进行更改之前触发

注意:

  • useInsertionEffect 适用于 CSS-in-JS 库作者。除非您正在开发 CSS-in-JS 库并且需要一个地方来注入样式,否则请使用 useEffectuseLayoutEffect
  • 无法从 useInsertionEffect 内部更新状态
  • useInsertionEffect 运行时,ref 尚未附加
  • useInsertionEffect 可以在 DOM 更新之前或之后运行,不应该依赖于在任何特定时间更新 DOM
  • useEffectuseLayoutEffect 不同的是,其他类型的效果会为每个效果触发清理,然后为每个效果进行设置,而 useInsertionEffect 将一次触发清理并设置一个组件。这导致清理和设置功能的“交错”

Performance Hooks

优化重新渲染性能的常见方法是跳过不必要的工作。例如重用缓存的计算,或者如果数据自上次渲染以来没有更改,则跳过重新渲染

要跳过计算和不必要的重新渲染:

  • useMemo:在重新渲染之间缓存昂贵的计算结果(记忆化)
  • useCallback:将函数定义传递给优化组件之前对其进行缓存

有时,无法跳过重新渲(因为屏幕实际上需要更新),在这种情况下可以通过将必须同步的阻塞更新(如输入内容)与不需要阻塞用户界面的非阻塞更新(如更新图表)分开来提高性能

要优先考虑渲染:

  • useTransition:将状态转换标记为非阻塞,并允许其他更新中断它
  • useDeferredValue:推迟更新 UI 的非关键部分,并让其他部分先更新

useMemo

const cachedValue = useMemo(calculateValue, dependencies)
  • calculateValue:计算要缓存的值的函数。它应该是纯的,不接受参数,返回任何类型的值。React 将缓存该返回值
  • cachedValue:当前值(被缓存的)

注意:

  • 不要过度“优化”:useMemo 本身的代价和缓存值计算的代价,孰轻孰重?

useCallback

const cachedFn = useCallback(fn, dependencies)

区别:

  • useMemo:运行第一个参数的函数,并缓存函数返回值
  • useCallback:缓存第一个参数的函数本身

useTransition

标记更新为低优先级的更新,低于 UI 更新(在不阻塞 UI 的情况下更新状态)

const [isPending, startTransition] = useTransition()
  • isPending:是否有待处理的转换
  • startTransition:函数,接收一个函数参数,将其中的状态更新标记为低优先级(不阻塞 UI)

示例:

function TabContainer() {
  const [isPending, startTransition] = useTransition();
  const [tab, setTab] = useState('about');
 
  function selectTab(nextTab) {
    startTransition(() => {
      setTab(nextTab);
    });
  }
  // ...
}

注意:

  • useTransition 是一个 Hook,因此只能在组件内部或自定义 Hook 中调用。如果需要在其他地方(例如,从数据库)启动 Transition,请改为调用独立的 startTransition
  • 仅当有权访问该状态的 set 函数时,才可以将更新包装到 Transition 中。如果想启动 Transition 来响应某些 prop 或自定义 Hook 值,请尝试 useDeferredValue
  • 传递给 startTransition 函数必须是同步的。 React 立即执行此函数,将其执行时发生的所有状态更新标记为转换。如果是异步的(例如,在定时器),它们将不会被标记为转换
  • 标记为 Transition 的状态更新将被其他状态更新中断
  • 转换更新不能用于控制文本输入
  • 如果有多个正在进行的转换,React 目前会将它们批处理在一起。这是一个限制,可能会在未来的版本中删除

useDeferredValue

推迟更新部分 UI

const deferredValue = useDeferredValue(value, initialValue?)
  • value :想要推迟的值,可以是任何类型
  • initialValue(实验特性):在组件初始渲染期间使用的值。如果省略,useDeferredValue 在初始渲染期间不会延迟,因为没有可以渲染的先前版本的 value
  • currentValue
    • 初始渲染期间:返回的延迟值将与提供的值相同
    • 更新期间:React 将首先尝试使用旧值重新渲染(因此它将返回旧值),然后在后台尝试使用新值进行另一次重新渲染(因此它将返回更新后的值)

示例:

import { useState, useDeferredValue } from 'react';
 
function SearchPage() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  // ...
}

注意:

  • 当更新位于 Transition 内时, useDeferredValue 始终返回新 value 并且不会生成延迟渲染,因为更新已经延迟
  • 传递给 useDeferredValue 的值应该是原始值或在渲染之外创建的对象。如果在渲染期间创建一个新对象并立即将其传递给 useDeferredValue,则每次渲染时它都会有所不同,从而导致不必要的后台重新渲染
  • useDeferredValue 接收到不同的值(Object.is)时,除了当前渲染(当它仍然使用以前的值时)之外,它还会使用新值在后台安排重新渲染。后台重新渲染是可中断的:如果 value 再次更新,React 将从头开始重新启动后台重新渲染。例如,如果用户输入内容的速度快于接收其延迟值的图表重新呈现的速度,则图表只会在用户停止输入后重新呈现
  • useDeferredValue<Suspense> 集成时,如果新值引起的后台更新暂停了 UI,用户将看不到回退。他们将看到旧的延迟值,直到数据加载
  • useDeferredValue 本身不会阻止额外的网络请求
  • useDeferredValue 本身不存在固定延迟。一旦 React 完成原始重新渲染,React 将立即开始使用新的延迟值进行后台重新渲染。由事件(例如打字)引起的任何更新都会中断后台重新渲染并优先于它
  • useDeferredValue 引起的背景重新渲染在提交到屏幕之前不会触发 Effect。如果后台重新渲染暂停,其 Effect 将在数据加载和 UI 更新后运行

Other Hooks

这些 Hook 对库作者来说最有用,但在应用程序代码中并不常用

useDebugValue

可以向 React DevTools 中的自定义 Hook 添加标签

useDebugValue(value, format?)
  • value:想要在 React DevTools 中显示的值,可以是任意类型
  • format:格式化函数。当检查组件时,React DevTools 将以该 value 作为参数调用格式化函数,然后显示返回的格式化值(可以是任何类型)。如果不指定格式化函数,则将显示原始 value 本身

useId

用于生成可以传递给可访问性属性的唯一 ID

const id = useId()
  • id:返回与此特定组件中的此特定 useId 调用关联的唯一 ID 字符串

注意:

  • useId 不应用于生成列表中的 keykey 应该根据数据生成

useSyncExternalStore

订阅外部存储

const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)
  • subscribe:采用单个 callback 参数并将其订阅到存储的函数。当 Store 发生变化时,它应该调用提供的 callback,这将导致组件重新渲染。subscribe 函数应该返回一个清理订阅的函数
  • getSnapshot:返回组件所需的存储中数据快照的函数。但存储未更改时,重复调用 getSnapshot 必须返回相同的值。如果存储发生变化并且返回值不同(Object.is),React 会重新渲染组件
  • getServerSnapshot:返回存储中数据的初始快照的函数。它将仅在服务器渲染期间以及客户端上服务器渲染内容的水合作用期间使用
    • 服务器快照在客户端和服务器之间必须相同,并且通常被序列化并从服务器传递到客户端。如果省略此参数,在服务器上渲染组件将引发错误
  • snapshot:Store 数据的当前快照

举例:

import { useSyncExternalStore } from 'react';
 
function subscribe(callback) {
  window.addEventListener('online', callback);
  window.addEventListener('offline', callback);
  return () => {
    window.removeEventListener('online', callback);
    window.removeEventListener('offline', callback);
  };
}
 
function getSnapshot() {
  return navigator.onLine;
}
 
function ChatIndicator() {
  const isOnline = useSyncExternalStore(subscribe, getSnapshot);
  // ...
}

注意:useSyncExternalStore 注意事项

useActionState(实验特性)

根据表单操作的结果更新状态

在组件的顶层调用 useActionState 来创建在调用表单操作时更新的组件状态。向 useActionState 传递现有的表单操作函数以及初始状态,它会返回在表单中使用的新操作以及最新的表单状态。最新的表单状态也会传递给您提供的函数

在早期的 React Canary 版本中,此 API 是 React DOM 的一部分,称为 useFormState

const [state, formAction] = useActionState(fn, initialState, permalink?);
  • fn:提交表单或按下按钮时要调用的函数
    • 第一个参数:表单的先前状态(最初是您传递的 initialState ,随后是其先前的返回值)
    • 表单数据(如:FormData
  • initialState:状态初始的值,可以是任何可序列化的值
  • permalink:包含此表单修改的唯一页面 URL 的字符串。用于具有动态内容(例如:提要)的页面以及渐进增强:如果 fn服务器操作并且表单在 JavaScript 包加载之前提交,则浏览器将导航到指定的永久链接 URL,而不是当前页面的永久链接 URL。确保在目标页面上呈现相同的表单组件(包括相同的操作 fnpermalink ),以便 React 知道如何传递状态。一旦形态被水合,该参数就不再起作用
  • state:目前的状态。第一次渲染是 initialState。调用操作后,它将匹配操作返回的值
  • formAction:一个新操作,可以将其作为 action 属性传递给 form 组件,或将 formAction 属性传递给表单中的任何 button 组件

举例:

import { useActionState } from "react";
 
async function increment(previousState, formData) {
  return previousState + 1;
}
 
function StatefulForm({}) {
  const [state, formAction] = useActionState(increment, 0);
  return (
    <form>
      {state}
      <button formAction={formAction}>Increment</button>
    </form>
  )
}

useOptimistic(实验特性)

乐观地更新 UI

const [optimisticState, addOptimistic] = useOptimistic(state, updateFn);
  • state:没有异步操作处于挂起时的最初状态
  • updateFn(currentState, optimisticValue):纯函数,通过两个参数得出新状态
    • 第一个参数 currentState:当前状态
    • 第二个参数 optimisticValue:调用 addOptimistic 传入的乐观值
    • 返回:新状态
  • optimisticState:产生的乐观状态
    • 有操作处于挂起状态:等于 updateFn 返回的值
    • 无操作处于挂起状态:等于 state
  • addOptimistic:接收一个参数,代表乐观更新的值
function App() {
  const [messages, setMessages] = useState([
    { text: "Hello there!", sending: false, key: 1 }
  ])
  const [optimisticMessages, addOptimisticMessage] = useOptimistic(
    messages,
    (state, newMessage) => [
      ...state,
      {
        text: newMessage,
        sending: true
      }
    ]
  );
  const formRef = useRef();
 
  async function formAction(formData) {
    addOptimisticMessage(formData.get("message")); // 乐观更新
    formRef.current.reset();
    异步请求(data => {
      setMessages((messages) => [...messages, { text: data }]);
    })
  }
 
  return (
    <>
      {optimisticMessages.map((message, index) => (
        <div key={index}>
          {message.text}
          {!!message.sending && <small>(发送中...)</small>}
        </div>
      ))}
      <form action={formAction} ref={formRef}>
        <input type="text" name="message" placeholder="Hello!" />
        <button type="submit">Send</button>
      </form>
    </>
  );
}