React官网Hook学习------上篇
本文记录的是React官网部分的内置Hook学习,也是我后面学习React源码部分的Hook实现的前置条件
Hook 可以帮助在组件中使用不同的 React 功能。你可以使用内置的 Hook 或使用自定义 Hook。本文列出了 React 中所有内置 Hook。
Hook概览
State Hook
状态帮助组件 "记住"用户输入的信息。例如,一个表单组件可以使用状态存储输入值,而一个图像库组件可以使用状态存储所选的图像索引。
使用以下 Hook 以向组件添加状态:
-
使用
useState声明可以直接更新的状态变量。 -
使用
useReducer在 reducer 函数 中声明带有更新逻辑的 state 变量。function ImageGallery() {
const [index, setIndex] = useState(0);
// ...
Context Hook
上下文帮助组件 从祖先组件接收信息,而无需将其作为 props 传递。例如,应用程序的顶层组件可以借助上下文将 UI 主题传递给所有下方的组件,无论这些组件层级有多深。
-
使用
useContext读取订阅上下文。function Button() {
const theme = useContext(ThemeContext);
// ...
Ref Hook
ref 允许组件 保存一些不用于渲染的信息,比如 DOM 节点或 timeout ID。与状态不同,更新 ref 不会重新渲染组件。ref 是从 React 范例中的"脱围机制"。当需要与非 React 系统如浏览器内置 API 一同工作时,ref 将会非常有用。
-
使用
useRef声明 ref。你可以在其中保存任何值,但最常用于保存 DOM 节点。 -
使用
useImperativeHandle自定义从组件中暴露的 ref,但是很少使用。function Form() {
const inputRef = useRef(null);
// ...
Effect Hook
Effect 允许组件 连接到外部系统并与之同步。这包括处理网络、浏览器、DOM、动画、使用不同 UI 库编写的小部件以及其他非 React 代码。
-
使用
useEffect将组件连接到外部系统。function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]);
// ...
Effect 是从 React 范式中的"脱围机制"。避免使用 Effect 协调应用程序的数据流。如果不需要与外部系统交互,那么 可能不需要 Effect。
useEffect 有两个很少使用的变换形式,它们在执行时机有所不同:
useLayoutEffect在浏览器重新绘制屏幕前执行,可以在此处测量布局。useInsertionEffect在 React 对 DOM 进行更改之前触发,库可以在此处插入动态 CSS。
性能 Hook
优化重新渲染性能的一种常见方法是跳过不必要的工作。例如,可以告诉 React 重用缓存的计算结果,或者如果数据自上次渲染以来没有更改,则跳过重新渲染。
可以使用以下 Hook 跳过计算和不必要的重新渲染:
-
使用
useMemo缓存计算代价昂贵的计算结果。 -
使用
useCallback将函数传递给优化组件之前缓存函数定义。function TodoList({ todos, tab, theme }) {
const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
// ...
}
有时由于屏幕确实需要更新,无法跳过重新渲染。在这种情况下,可以通过将必须同步的阻塞更新(比如使用输入法输入内容)与不需要阻塞用户界面的非阻塞更新(比如更新图表)分离以提高性能。
使用以下 Hook 处理渲染优先级:
useTransition允许将状态转换标记为非阻塞,并允许其他更新中断它。useDeferredValue允许延迟更新 UI 的非关键部分,以让其他部分先更新。
1、useActionState
useActionState 是一个可以根据某个表单动作的结果更新 state 的 Hook。
const [state, formAction, isPending] = useActionState(fn, initialState, permalink?);
在组件的顶层调用 useActionState 即可创建一个随 表单动作被调用 而更新的 state。在调用 useActionState 时在参数中传入现有的表单动作函数以及一个初始状态,无论 Action 是否在 pending 中,它都会返回一个新的 action 函数和一个 form state 以供在 form 中使用。这个新的 form state 也会作为参数传入提供的表单动作函数。
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}>+1</button>
</form>
)
}
form state 是一个只在表单被提交触发 action 后才会被更新的值。如果该表单没有被提交,该值会保持传入的初始值不变。
如果与服务器函数一起使用,useActionState 允许与表单交互的服务器的返回值在激活完成前显示。
参数
fn:当按钮被按下或者表单被提交时触发的函数。当函数被调用时,该函数会接收到表单的上一个 state(初始值为传入的initialState参数,否则为上一次执行完该函数的结果)作为函数的第一个参数,余下参数为普通表单动作接到的参数。initialState:state 的初始值。任何可序列化的值都可接收。当 action 被调用一次后该参数会被忽略。- 可选的
permalink:一个包含了在特定情况下(后述)表单提交后将跳转到的独立 URL 的字符串。此参数用于渐进式地增强应用了动态内容的页面(例如 feeds):如果fn是一个 服务器函数,并且表单在 JavaScript 包加载之前提交,则浏览器将导航到指定的permalinkURL,而不是当前页面的 URL。确保在目标页面上渲染相同的表单组件(包括相同的fn和permalink),以便 React 知道应如何同步状态。一旦表单被激活,此参数将不再起作用。
返回值
useActionState 返回一个包含以下值的数组:
- 当前的 state。第一次渲染期间,该值为传入的
initialState参数值。在 action 被调用后该值会变为 action 的返回值。 - 一个新的 action 函数用于在你的
form组件的action参数或表单中任意一个button组件的formAction参数中传递。这个 action 也可以手动在startTransition中调用。 - 一个
isPending标识,用于表明是否有正在 pending 的 Transition。
注意
- 在支持 React 服务器组件的框架中使用该功能时,
useActionState允许表单在服务器渲染阶段时获得部分交互性。当不使用服务器组件时,它的特性与本地 state 相同。 - 与直接通过表单动作调用的函数不同,传入
useActionState的函数被调用时,会多传入一个代表 state 的上一个值或初始值的参数作为该函数的第一个参数。
用法
使用某个表单动作返回的信息
在组件的顶层调用 useActionState 以获取上一次表单被提交时触发的 action 的返回值。
import { useActionState } from 'react';
import { action } from './actions.js';
function MyComponent() {
const [state, formAction] = useActionState(action, null);
// ...
return (
<form action={formAction}>
{/* ... */}
</form>
);
}
useActionState 返回一个包含以下值的数组:
- 该表单的 当前 state,初始值为提供的 初始 state,当表单被提交后则改为传入的 action 的返回值。
- 传入
<form>标签的action属性的 新 action,或者手动在startTransition中调用它。 - 一个 pending state,可以在处理 action 的过程中使用它。
表单被提交后,传入的 action 函数会被执行。返回值将会作为该表单的新的 当前 state。
传入的 action 接受到的第一个参数将会变为该表单的 当前 state。当表单第一次被提交时将会传入提供的 初始 state,之后都将传入上一次调用 action 函数的返回值。余下参数与未使用 useActionState 前接受的参数别无二致。
例子
App.js
import { useActionState, useState } from "react";
import { addToCart } from "./actions.js";
function AddToCartForm({itemID, itemTitle}) {
const [message, formAction, isPending] = useActionState(addToCart, null);
return (
<form action={formAction}>
<h2>{itemTitle}</h2>
<input type="hidden" name="itemID" value={itemID} />
<button type="submit">加入购物车</button>
{isPending ? "加载中......" : message}
</form>
);
}
export default function App() {
return (
<>
<AddToCartForm itemID="1" itemTitle="JavaScript:权威指南" />
<AddToCartForm itemID="2" itemTitle="JavaScript:优点荟萃" />
</>
)
}
actions.js
"use server";
export async function addToCart(prevState, queryData) {
const itemID = queryData.get('itemID');
if (itemID === "1") {
return "已加入购物车";
} else {
// 人为添加延迟以使等待更明显。
await new Promise(resolve => {
setTimeout(resolve, 2000);
});
return "无法加入购物车:商品已售罄";
}
}
2、useCallback
useCallback 是一个允许你在多次渲染中缓存函数的 React Hook。
const cachedFn = useCallback(fn, dependencies)
参考
useCallback(fn, dependencies)
在组件顶层调用 useCallback 以便在多次渲染中缓存函数:
import { useCallback } from 'react';
export default function ProductPage({ productId, referrer, theme }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
参数
fn:想要缓存的函数。此函数可以接受任何参数并且返回任何值。在初次渲染时,React 将把函数返回给你(而不是调用它!)。当进行下一次渲染时,如果dependencies相比于上一次渲染时没有改变,那么 React 将会返回相同的函数。否则,React 将返回在最新一次渲染中传入的函数,并且将其缓存以便之后使用。React 不会调用此函数,而是返回此函数。你可以自己决定何时调用以及是否调用。dependencies:有关是否更新fn的所有响应式值的一个列表。响应式值包括 props、state,和所有在你组件内部直接声明的变量和函数。如果你的代码检查工具 配置了 React,那么它将校验每一个正确指定为依赖的响应式值。依赖列表必须具有确切数量的项,并且必须像[dep1, dep2, dep3]这样编写。React 使用Object.is比较每一个依赖和它的之前的值。
返回值
在初次渲染时,useCallback 返回你已经传入的 fn 函数
在之后的渲染中, 如果依赖没有改变,useCallback 返回上一次渲染中缓存的 fn 函数;否则返回这一次渲染传入的 fn。
注意
useCallback是一个 Hook,所以应该在 组件的顶层 或自定义 Hook 中调用。你不应在循环或者条件语句中调用它。如果你需要这样做,请新建一个组件,并将 state 移入其中。- 除非有特定的理由,React 将不会丢弃已缓存的函数 。例如,在开发中,当编辑组件文件时,React 会丢弃缓存。在生产和开发环境中,如果你的组件在初次挂载中暂停,React 将会丢弃缓存。在未来,React 可能会增加更多利用了丢弃缓存机制的特性。例如,如果 React 未来内置了对虚拟列表的支持,那么在滚动超出虚拟化表视口的项目时,抛弃缓存是有意义的。如果你依赖
useCallback作为一个性能优化途径,那么这些对你会有帮助。否则请考虑使用 state 变量 或 ref。
用法
跳过组件的重新渲染
当你优化渲染性能的时候,有时需要缓存传递给子组件的函数。让我们先关注一下如何实现,稍后去理解在哪些场景中它是有用的。
为了缓存组件中多次渲染的函数,你需要将其定义在 useCallback Hook 中:
import { useCallback } from 'react';
function ProductPage({ productId, referrer, theme }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
// ...
你需要传递两个参数给 useCallback:
- 在多次渲染中需要缓存的函数
- 函数内部需要使用到的所有组件内部值的 依赖列表。
初次渲染时,在 useCallback 处接收的 返回函数 将会是已经传入的函数。
在之后的渲染中,React 将会使用 Object.is 把 当前的依赖 和已传入之前的依赖进行比较。如果没有任何依赖改变,useCallback 将会返回与之前一样的函数。否则 useCallback 将返回 此次 渲染中传递的函数。
简而言之,useCallback 在多次渲染中缓存一个函数,直至这个函数的依赖发生改变。
从记忆化回调中更新 state
有时,你可能在记忆化回调中基于之前的 state 来更新 state。
下面的 handleAddTodo 函数将 todos 指定为依赖项,因为它会从中计算下一个 todos:
function TodoList() {
const [todos, setTodos] = useState([]);
const handleAddTodo = useCallback((text) => {
const newTodo = { id: nextId++, text };
setTodos([...todos, newTodo]);
}, [todos]);
// ...
我们期望记忆化函数具有尽可能少的依赖,当你读取 state 只是为了计算下一个 state 时,你可以通过传递 updater function 以移除该依赖:
function TodoList() {
const [todos, setTodos] = useState([]);
const handleAddTodo = useCallback((text) => {
const newTodo = { id: nextId++, text };
setTodos(todos => [...todos, newTodo]);
}, []); // ✅ 不需要 todos 依赖项
// ...
在这里,并不是将 todos 作为依赖项并在内部读取它,而是传递一个关于 如何 更新 state 的指示器 (todos => [...todos, newTodo]) 给 React。
防止频繁触发 Effect
有时,你想要在 Effect 内部调用函数:
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
function createOptions() {
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}
useEffect(() => {
const options = createOptions();
const connection = createConnection(options);
connection.connect();
// ...
这会产生一个问题,每一个响应值都必须声明为 Effect 的依赖。但是如果将 createOptions 声明为依赖,它会导致 Effect 不断重新连接到聊天室:
useEffect(() => {
const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // 问题:这个依赖在每一次渲染中都会发生改变
// ...
为了解决这个问题,需要在 Effect 中将要调用的函数包裹在 useCallback 中:
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
const createOptions = useCallback(() => {
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}, [roomId]); // 仅当 roomId 更改时更改
useEffect(() => {
const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // 仅当 createOptions 更改时更改
// ...
这将确保如果 roomId 相同,createOptions 在多次渲染中会是同一个函数。但是,最好消除对函数依赖项的需求 。将你的函数移入 Effect 内部:
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
useEffect(() => {
function createOptions() { // ✅ 无需使用回调或函数依赖!
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}
const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ 仅当 roomId 更改时更改
// ...
现在你的代码变得更简单了并且不需要 useCallback。
优化自定义 Hook
如果你正在编写一个 自定义 Hook,建议将它返回的任何函数包裹在 useCallback 中:
function useRouter() {
const { dispatch } = useContext(RouterStateContext);
const navigate = useCallback((url) => {
dispatch({ type: 'navigate', url });
}, [dispatch]);
const goBack = useCallback(() => {
dispatch({ type: 'back' });
}, [dispatch]);
return {
navigate,
goBack,
};
}
3、useContext
useContext 是一个 React Hook,可以让你读取和订阅组件中的 context。
const value = useContext(SomeContext)
参考
useContext(SomeContext)
在组件的顶层调用 useContext 来读取和订阅 context。
import { useContext } from 'react';
function MyComponent() {
const theme = useContext(ThemeContext);
// ...
参数
SomeContext:先前用createContext创建的 context。context 本身不包含信息,它只代表你可以提供或从组件中读取的信息类型。
返回值
useContext 为调用组件返回 context 的值。它被确定为传递给树中调用组件上方最近的 SomeContext 的 value。如果没有这样的 provider,那么返回值将会是为创建该 context 传递给 createContext 的 defaultValue。返回的值始终是最新的。如果 context 发生变化,React 会自动重新渲染读取 context 的组件。
注意事项
- 组件中的
useContext()调用不受 同一 组件返回的 provider 的影响。相应的<Context>需要位于调用useContext()的组件 之上。 - 从 provider 接收到不同的
value开始,React 自动重新渲染使用了该特定 context 的所有子级。先前的值和新的值会使用Object.is来做比较。使用memo来跳过重新渲染并不妨碍子级接收到新的 context 值。 - 如果你的构建系统在输出中产生重复的模块(可能发生在符号链接中),这可能会破坏 context。通过 context 传递数据只有在用于传递 context 的
SomeContext和用于读取数据的SomeContext是完全相同的对象时才有效,这是由===比较决定的。
用法
向组件树深层传递数据
在组件的最顶级调用 useContext 来读取和订阅 context。
import { useContext } from 'react';
function Button() {
const theme = useContext(ThemeContext);
// ...
useContext 返回你向 context 传递的 context value。为了确定 context 值,React 搜索组件树,为这个特定的 context 向上查找最近的 context provider。
若要将 context 传递给 Button,请将其或其父组件之一包装到相应的 context provider:
function MyPage() {
return (
<ThemeContext value="dark">
<Form />
</ThemeContext>
);
}
function Form() {
// ... 在内部渲染 buttons ...
}
provider 和 Button 之间有多少层组件并不重要。当 Form 中的任何位置的 Button 调用 useContext(ThemeContext) 时,它都将接收 "dark" 作为值。
App.js
import { createContext, useContext } from 'react';
const ThemeContext = createContext(null);
export default function MyApp() {
return (
<ThemeContext value="dark">
<Form />
</ThemeContext>
)
}
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>
)
}
function Button({ children }) {
const theme = useContext(ThemeContext);
const className = 'button-' + theme;
return (
<button className={className}>
{children}
</button>
);
}
通过 context 更新传递的数据
通常,你会希望 context 随着时间的推移而改变。要更新 context,请将其与 state 结合。在父组件中声明一个状态变量,并将当前状态作为 context value 传递给 provider。
function MyPage() {
const [theme, setTheme] = useState('dark');
return (
<ThemeContext value={theme}>
<Form />
<Button onClick={() => {
setTheme('light');
}}>
Switch to light theme
</Button>
</ThemeContext>
);
}
现在 provider 中的任何一个 Button 都会接收到当前的 theme 值。如果调用 setTheme 来更新传递给 provider 的 theme 值,则所有 Button 组件都将使用新的值 'light' 来重新渲染。
4、useDebugValue
useDebugValue 是一个 React Hook,可以让你在 React 开发工具 中为自定义 Hook 添加标签。
useDebugValue(value, format?)
参考
useDebugValue(value, format?)
在你的 自定义 Hook 的顶层调用 useDebugValue,以显示可读的调试值:
import { useDebugValue } from 'react';
function useOnlineStatus() {
// ...
useDebugValue(isOnline ? 'Online' : 'Offline');
// ...
}
参数
value:你想在 React 开发工具中显示的值。可以是任何类型。- 可选
format:它接受一个格式化函数。当组件被检查时,React 开发工具将用value作为参数来调用格式化函数,然后显示返回的格式化值(可以是任何类型)。如果不指定格式化函数,则会显示value。
返回值
useDebugValue 没有返回值。
5、useDeferredValue
useDeferredValue 是一个 React Hook,可以让你延迟更新 UI 的某些部分。
const deferredValue = useDeferredValue(value)
参考
useDeferredValue(value, initialValue?)
在组件的顶层调用 useDeferredValue 来获取该值的延迟版本。
import { useState, useDeferredValue } from 'react';
function SearchPage() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
// ...
}
参数
value: 你想延迟的值,可以是任何类型。- 可选的
initialValue: 组件初始渲染时使用的值。如果省略此选项,useDeferredValue在初始渲染期间不会延迟,因为没有以前的版本可以渲染。
返回值
currentValue: 在初始渲染期间,返回的延迟值是initialValue或你提供的值。在更新期间,React 首先尝试使用旧值重新渲染(因此返回旧值),然后在后台尝试使用新值重新渲染(因此返回更新后的值)。
注意事项
- 当更新发生在 Transition 内部时,
useDeferredValue总是返回新的value并且不会产生延迟渲染,因为该更新已经被延迟了。 - 传递给
useDeferredValue的值应该是原始值(如字符串和数字)或是在渲染之外创建的对象。如果你在渲染期间创建一个新对象并立即将其传递给useDeferredValue,它在每次渲染时都会不同,从而导致不必要的后台重新渲染。 - 当
useDeferredValue接收到与之前不同的值(使用Object.is进行比较)时,除了当前渲染(此时它仍然使用旧值),它还会安排一个后台重新渲染。这个后台重新渲染是可以被中断的,如果value有新的更新,React 会从头开始重新启动后台渲染。举个例子,如果用户在输入框中的输入速度比接收延迟值的图表重新渲染的速度快,那么图表只会在用户停止输入后重新渲染。 useDeferredValue与 `` 集成。如果由于新值引起的后台更新导致 UI 暂停,用户将不会看到后备方案。他们将看到旧的延迟值,直到数据加载完成。useDeferredValue本身并不能阻止额外的网络请求。useDeferredValue本身不会引起任何固定的延迟。一旦 React 完成原始的重新渲染,它会立即开始使用新的延迟值处理后台重新渲染。由事件(例如输入)引起的任何更新都会中断后台重新渲染,并被优先处理。- 由
useDeferredValue引起的后台重新渲染在提交到屏幕之前不会触发 Effect。如果后台重新渲染被暂停,Effect 将在数据加载后和 UI 更新后运行。
用法
在新内容加载期间显示旧内容。
在组件的顶层调用 useDeferredValue 来延迟更新 UI 的某些部分。
import { useState, useDeferredValue } from 'react';
function SearchPage() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
// ...
}
在初始渲染期间,返回的 延迟值 与你提供的 值 相同。
在更新期间,延迟值 会"滞后于"最新的 值。具体地说,React 首先会在不更新延迟值的情况下进行重新渲染,然后在后台尝试使用新接收到的值进行重新渲染。
这个例子中,在获取搜索结果时,SearchResults 组件会 suspend。尝试输入 "a",等待结果出现后,将其编辑为 "ab"。此时 "a" 的结果会被加载中的后备方案替代。
App.js
import { Suspense, useState } from 'react';
import SearchResults from './SearchResults.js';
export default function App() {
const [query, setQuery] = useState('');
return (
<>
<label>
Search albums:
<input value={query} onChange={e => setQuery(e.target.value)} />
</label>
<Suspense fallback={<h2>Loading...</h2>}>
<SearchResults query={query} />
</Suspense>
</>
);
}
SearchResults.js
import {use} from 'react';
import { fetchData } from './data.js';
export default function SearchResults({ query }) {
if (query === '') {
return null;
}
const albums = use(fetchData(`/search?q=${query}`));
if (albums.length === 0) {
return <p>No matches for <i>"{query}"</i></p>;
}
return (
<ul>
{albums.map(album => (
<li key={album.id}>
{album.title} ({album.year})
</li>
))}
</ul>
);
}
一个常见的备选 UI 模式是 延迟 更新结果列表,并继续显示之前的结果,直到新的结果准备好。调用 useDeferredValue 并将延迟版本的查询参数向下传递:
export default function App() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
return (
<>
<label>
Search albums:
<input value={query} onChange={e => setQuery(e.target.value)} />
</label>
<Suspense fallback={<h2>Loading...</h2>}>
<SearchResults query={deferredQuery} />
</Suspense>
</>
);
}
query 会立即更新,所以输入框将显示新值。然而,deferredQuery 在数据加载完成前会保留以前的值,因此 SearchResults 将暂时显示旧的结果。
在下面的示例中,输入 "a",等待结果加载完成,然后将输入框编辑为 "ab"。注意,现在你看到的不是 suspense 后备方案,而是旧的结果列表,直到新的结果加载完成:
import { Suspense, useState, useDeferredValue } from 'react';
import SearchResults from './SearchResults.js';
export default function App() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
return (
<>
<label>
Search albums:
<input value={query} onChange={e => setQuery(e.target.value)} />
</label>
<Suspense fallback={<h2>Loading...</h2>}>
<SearchResults query={deferredQuery} />
</Suspense>
</>
);
}
SearchResults.js
import {use} from 'react';
import { fetchData } from './data.js';
export default function SearchResults({ query }) {
if (query === '') {
return null;
}
const albums = use(fetchData(`/search?q=${query}`));
if (albums.length === 0) {
return <p>No matches for <i>"{query}"</i></p>;
}
return (
<ul>
{albums.map(album => (
<li key={album.id}>
{album.title} ({album.year})
</li>
))}
</ul>
);
}
表明内容已过时
在上面的示例中,当最新的查询结果仍在加载时,没有任何提示。如果新的结果需要一段时间才能加载完成,这可能会让用户感到困惑。为了更明显地告知用户结果列表与最新查询不匹配,你可以在显示旧的查询结果时添加一个视觉提示:
<div style={{
opacity: query !== deferredQuery ? 0.5 : 1,
}}>
<SearchResults query={deferredQuery} />
</div>
有了上面这段代码,当你开始输入时,旧的结果列表会略微变暗,直到新的结果列表加载完毕。你也可以添加 CSS 过渡来延迟变暗的过程,让用户感受到一种渐进式的过渡,就像下面的例子一样:
App.js
import { Suspense, useState, useDeferredValue } from 'react';
import SearchResults from './SearchResults.js';
export default function App() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
return (
<>
<label>
Search albums:
<input value={query} onChange={e => setQuery(e.target.value)} />
</label>
<Suspense fallback={<h2>Loading...</h2>}>
<div style={{
opacity: isStale ? 0.5 : 1,
transition: isStale ? 'opacity 0.2s 0.2s linear' : 'opacity 0s 0s linear'
}}>
<SearchResults query={deferredQuery} />
</div>
</Suspense>
</>
);
}
SearchResults.js
import {use} from 'react';
import { fetchData } from './data.js';
export default function SearchResults({ query }) {
if (query === '') {
return null;
}
const albums = use(fetchData(`/search?q=${query}`));
if (albums.length === 0) {
return <p>No matches for <i>"{query}"</i></p>;
}
return (
<ul>
{albums.map(album => (
<li key={album.id}>
{album.title} ({album.year})
</li>
))}
</ul>
);
}
6、useEffect
useEffect 是一个 React Hook,它允许你 将组件与外部系统同步。
useEffect(setup, dependencies?)
参考
useEffect(setup, dependencies?)
在组件的顶层调用 useEffect 来声明一个 Effect:
import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);
// ...
}
参数
setup:处理 Effect 的函数。setup 函数选择性返回一个 清理(cleanup) 函数。当组件被添加到 DOM 的时候,React 将运行 setup 函数。在每次依赖项变更重新渲染后,React 将首先使用旧值运行 cleanup 函数(如果你提供了该函数),然后使用新值运行 setup 函数。在组件从 DOM 中移除后,React 将最后一次运行 cleanup 函数。- 可选
dependencies:setup代码中引用的所有响应式值的列表。响应式值包括 props、state 以及所有直接在组件内部声明的变量和函数。如果你的代码检查工具 配置了 React,那么它将验证是否每个响应式值都被正确地指定为一个依赖项。依赖项列表的元素数量必须是固定的,并且必须像[dep1, dep2, dep3]这样内联编写。React 将使用Object.is来比较每个依赖项和它先前的值。如果省略此参数,则在每次重新渲染组件之后,将重新运行 Effect 函数。如果你想了解更多,请参见 传递依赖数组、空数组和不传递依赖项之间的区别。
返回值
useEffect 返回 undefined。
注意事项
useEffect是一个 Hook,因此只能在 组件的顶层 或自己的 Hook 中调用它,而不能在循环或者条件内部调用。如果需要,抽离出一个新组件并将 state 移入其中。- 如果你 没有打算与某个外部系统同步 ,那么你可能不需要 Effect。
- 当严格模式启动时,React 将在真正的 setup 函数首次运行前,运行一个开发模式下专有的额外 setup + cleanup 周期 。这是一个压力测试,用于确保 cleanup 逻辑"映射"到了 setup 逻辑,并停止或撤消 setup 函数正在做的任何事情。如果这会导致一些问题,请实现 cleanup 函数。
- 如果你的一些依赖项是组件内部定义的对象或函数,则存在这样的风险,即它们将 导致 Effect 过多地重新运行 。要解决这个问题,请删除不必要的 对象 和 函数 依赖项。你还可以 抽离状态更新 和 非响应式的逻辑 到 Effect 之外。
- 如果你的 Effect 不是由交互(比如点击)引起的,那么 React 会让浏览器 在运行 Effect 前先绘制出更新后的屏幕 。如果你的 Effect 正在做一些视觉相关的事情(例如,定位一个 tooltip),并且有显著的延迟(例如,它会闪烁),那么将
useEffect替换为useLayoutEffect。 - 如果你的 Effect 是由一个交互(比如点击)引起的,React 可能会在浏览器重新绘制屏幕之前执行 Effect 。通常情况下,这样是符合预期的。但是,如果你必须要推迟 Effect 执行到浏览器绘制之后,和使用
alert()类似,可以使用setTimeout。 - 即使你的 Effect 是由一个交互(比如点击)引起的,React 也可能允许浏览器在处理 Effect 内部的状态更新之前重新绘制屏幕 。通常,这样是符合预期的。但是,如果你一定要阻止浏览器重新绘制屏幕,则需要用
useLayoutEffect替换useEffect。 - Effect 只在客户端上运行,在服务端渲染中不会运行。
7、useEffectEvent
useEffectEvent 是一个 React Hook,它可以让你将 Effect 中的非响应式逻辑提取到一个可复用的函数中,这个函数称为 Effect Event。
const onSomething = useEffectEvent(callback)
参考
useEffectEvent(callback)
在组件的顶层调用 useEffectEvent 来声明一个 Effect Event。Effect Event 是你可以在 Effect 中调用的函数,例如 useEffect:
import { useEffectEvent, useEffect } from 'react';
function ChatRoom({ roomId, theme }) {
const onConnected = useEffectEvent(() => {
showNotification('已连接!', theme);
});
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
onConnected();
});
connection.connect();
return () => connection.disconnect();
}, [roomId]);
// ...
}
参数
callback:一个包含你 Effect Event 逻辑的函数。当你使用useEffectEvent定义一个 Effect Event 时,callback在被调用时总是可以访问到最新的 props 和 state。这有助于避免陈旧闭包问题。
返回值
返回一个 Effect Event 函数。你可以在 useEffect、useLayoutEffect 或 useInsertionEffect 中调用这个函数。
将强制执行此限制,以防止在错误的上下文中调用效果事件。
注意事项
- 仅在 Effect 中调用 :Effect Event 应该只在 Effect 中调用。在使用它的 Effect 之前定义它。不要将它传递给其他组件或 hooks。
eslint-plugin-react-hookslinter(6.1.1 或者更高版本)将强制执行此限制,以防止在错误的上下文中调用 Effect Events。 - 不是依赖数组的捷径 :不要用
useEffectEvent来避免在 Effect 的依赖数组中声明依赖。这可能会隐藏 bug 并让代码更难理解。更推荐显式依赖,或使用 ref 来比较之前的值。 - 用于非响应式逻辑 :仅在逻辑不依赖变化的值时使用
useEffectEvent来提取。
用法
读取最新的 props 和 state
通常,当你在 Effect 中访问一个响应式值时,你必须把它包含在依赖数组里。这样可以确保当这个值改变时,Effect 会再次运行,这通常是期望的行为。
但在某些情况下,你可能只想在 Effect 中读取最新的 props 或 state,而不希望当这些值改变时让 Effect 重新运行。
要在 Effect 中读取最新的 props 或 state,而不让这些值成为响应式依赖,请把它们放进一个 Effect Event 中。
import { useEffect, useContext, useEffectEvent } from 'react';
function Page({ url }) {
const { items } = useContext(ShoppingCartContext);
const numberOfItems = items.length;
const onNavigate = useEffectEvent((visitedUrl) => {
logVisit(visitedUrl, numberOfItems);
});
useEffect(() => {
onNavigate(url);
}, [url]);
// ...
}
在本例中,当 url 发生变化时,Effect 应在呈现后重新运行(以记录新页面的访问),但当 numberOfItems 发生变化时,它 不 应该重新运行。通过将日志记录逻辑封装在一个 Effect 事件中,numberOfItems 就变成了非响应的。它总是从最新值读取,而不会触发 Effect。
你可以将 url 等响应式值作为参数传递给 Effect Event,使其保持响应状态,同时在事件内部访问最新的非响应式值。
8、useId
useId 是一个 React Hook,可以生成传递给无障碍属性的唯一 ID。
const id = useId()
参考
useId()
在组件的顶层调用 useId 生成唯一 ID:
import { useId } from 'react';
function PasswordField() {
const passwordHintId = useId();
// ...
参数
useId 不带任何参数。
返回值
useId 返回一个唯一的字符串 ID,与此特定组件中的 useId 调用相关联。
注意事项
useId是一个 Hook,因此你只能 在组件的顶层 或自己的 Hook 中调用它。你不能在内部循环或条件判断中调用它。如果需要,可以提取一个新组件并将 state 移到该组件中。useId不应该被用来生成列表中的 key 。key 应该由你的数据生成。useId当前无法在 异步服务器组件 中使用。
9、useImperativeHandle
useImperativeHandle 是 React 中的一个 Hook,它能让你自定义由 ref 暴露出来的句柄。
useImperativeHandle(ref, createHandle, dependencies?)
参考
useImperativeHandle(ref, createHandle, dependencies?)
在组件顶层通过调用 useImperativeHandle 来自定义 ref 暴露出来的句柄:
import { useImperativeHandle } from 'react';
function MyInput({ ref }) {
useImperativeHandle(ref, () => {
return {
// ... 你的方法 ...
};
}, []);
// ...
参数
ref:该ref是你从MyInput组件的 prop 中提取的参数。createHandle:该函数无需参数,它返回你想要暴露的 ref 的句柄。该句柄可以包含任何类型。通常,你会返回一个包含你想暴露的方法的对象。- 可选的
dependencies:函数createHandle代码中所用到的所有反应式的值的列表。反应式的值包含 props、状态和其他所有直接在你组件体内声明的变量和函数。倘若你的代码检查器已 为 React 配置好,它会验证每一个反应式的值是否被正确指定为依赖项。该列表的长度必须是一个常数项,并且必须按照[dep1, dep2, dep3]的形式罗列各依赖项。React 会使用Object.is来比较每一个依赖项与其对应的之前值。如果一次重新渲染导致某些依赖项发生了改变,或你没有提供这个参数列表,你的函数createHandle将会被重新执行,而新生成的句柄则会被分配给 ref。
从 React 19 开始,
ref可作为 prop 使用 。在 React 18 及更早版本中,需要通过forwardRef来获取ref。
返回值
useImperativeHandle 返回 undefined。
使用方法
向父组件暴露一个自定义的 ref 句柄
App.js
import { useRef } from 'react';
import MyInput from './MyInput.js';
export default function Form() {
const ref = useRef(null);
function handleClick() {
ref.current.focus();
// 下方代码不起作用,因为 DOM 节点并未被暴露出来:
// ref.current.style.opacity = 0.5;
}
return (
<form>
<MyInput placeholder="Enter your name" ref={ref} />
<button type="button" onClick={handleClick}>
Edit
</button>
</form>
);
}
MyInput.js
import { useRef, useImperativeHandle } from 'react';
function MyInput({ ref, ...props }) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => {
return {
focus() {
inputRef.current.focus();
},
scrollIntoView() {
inputRef.current.scrollIntoView();
},
};
}, []);
return <input {...props} ref={inputRef} />;
};
export default MyInput;
暴露你自己的命令式方法
你通过命令式句柄暴露出来的方法不一定需要完全匹配 DOM 节点的方法。例如,这个 Post 组件暴露了一个 scrollAndFocusAddComment 方法。它可以让你在点击按钮后,使父组件 Page 滚动到评论列表的底部 并 聚焦到输入框:
App.js
import { useRef } from 'react';
import Post from './Post.js';
export default function Page() {
const postRef = useRef(null);
function handleClick() {
postRef.current.scrollAndFocusAddComment();
}
return (
<>
<button onClick={handleClick}>
Write a comment
</button>
<Post ref={postRef} />
</>
);
}
Post.js
import { useRef, useImperativeHandle } from 'react';
import CommentList from './CommentList.js';
import AddComment from './AddComment.js';
function Post({ ref }) {
const commentsRef = useRef(null);
const addCommentRef = useRef(null);
useImperativeHandle(ref, () => {
return {
scrollAndFocusAddComment() {
commentsRef.current.scrollToBottom();
addCommentRef.current.focus();
}
};
}, []);
return (
<>
<article>
<p>Welcome to my blog!</p>
</article>
<CommentList ref={commentsRef} />
<AddComment ref={addCommentRef} />
</>
);
};
export default Post;
CommentList.js
import { useRef, useImperativeHandle } from 'react';
function CommentList({ ref }) {
const divRef = useRef(null);
useImperativeHandle(ref, () => {
return {
scrollToBottom() {
const node = divRef.current;
node.scrollTop = node.scrollHeight;
}
};
}, []);
let comments = [];
for (let i = 0; i < 50; i++) {
comments.push(<p key={i}>Comment #{i}</p>);
}
return (
<div className="CommentList" ref={divRef}>
{comments}
</div>
);
}
export default CommentList;
AddComment.js
import { useRef, useImperativeHandle } from 'react';
function AddComment({ ref }) {
return <input placeholder="Add comment..." ref={ref} />;
}
export default AddComment;