React 自带了一些内置的 Hook,例如 useState
、useContext
和 useEffect
。有时,你可能希望有一个 Hook 能用于更具体的用途:例如,获取数据、跟踪用户是否在线或连接到聊天室。你可能无法在 React 中找到这些 Hook,但你可以根据应用程序的需要创建自己的 Hook。
你将学习
- 什么是自定义 Hook,以及如何编写自己的 Hook
- 如何在组件之间复用逻辑
- 如何命名和构建你的自定义 Hook
- 何时以及为何提取自定义 Hook
自定义 Hook:在组件之间共享逻辑
假设你正在开发一个严重依赖网络的应用程序(大多数应用程序都是如此)。你希望在用户使用应用程序时,如果他们的网络连接意外断开,就向他们发出警告。你会怎么做?看起来你的组件中需要两样东西
这将使你的组件与网络状态保持 同步。你可能会从以下代码开始
import { useState, useEffect } from 'react'; export default function StatusBar() { const [isOnline, setIsOnline] = useState(true); useEffect(() => { function handleOnline() { setIsOnline(true); } function handleOffline() { setIsOnline(false); } window.addEventListener('online', handleOnline); window.addEventListener('offline', handleOffline); return () => { window.removeEventListener('online', handleOnline); window.removeEventListener('offline', handleOffline); }; }, []); return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>; }
尝试打开和关闭网络,并注意 StatusBar
如何响应你的操作而更新。
现在假设你还想在另一个组件中使用相同的逻辑。你想要实现一个“保存”按钮,当网络断开时,该按钮将变为禁用状态,并显示“重新连接…”而不是“保存”。
首先,你可以将 isOnline
状态和 Effect 复制并粘贴到 SaveButton
中
import { useState, useEffect } from 'react'; export default function SaveButton() { const [isOnline, setIsOnline] = useState(true); useEffect(() => { function handleOnline() { setIsOnline(true); } function handleOffline() { setIsOnline(false); } window.addEventListener('online', handleOnline); window.addEventListener('offline', handleOffline); return () => { window.removeEventListener('online', handleOnline); window.removeEventListener('offline', handleOffline); }; }, []); function handleSaveClick() { console.log('✅ Progress saved'); } return ( <button disabled={!isOnline} onClick={handleSaveClick}> {isOnline ? 'Save progress' : 'Reconnecting...'} </button> ); }
验证一下,如果你关闭网络,按钮的外观是否会发生变化。
这两个组件工作正常,但它们之间逻辑的重复令人遗憾。似乎即使它们的视觉外观不同,你也想在它们之间复用逻辑。
从组件中提取你自己的自定义 Hook
假设与 useState
和 useEffect
类似,有一个内置的 useOnlineStatus
Hook。那么这两个组件都可以简化,你可以消除它们之间的重复
function StatusBar() {
const isOnline = useOnlineStatus();
return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}
function SaveButton() {
const isOnline = useOnlineStatus();
function handleSaveClick() {
console.log('✅ Progress saved');
}
return (
<button disabled={!isOnline} onClick={handleSaveClick}>
{isOnline ? 'Save progress' : 'Reconnecting...'}
</button>
);
}
虽然没有这样的内置 Hook,但你可以自己编写。声明一个名为 useOnlineStatus
的函数,并将所有重复的代码从你之前编写的组件中移到该函数中
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
function handleOnline() {
setIsOnline(true);
}
function handleOffline() {
setIsOnline(false);
}
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return isOnline;
}
在函数的末尾,返回 isOnline
。这允许你的组件读取该值
import { useOnlineStatus } from './useOnlineStatus.js'; function StatusBar() { const isOnline = useOnlineStatus(); return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>; } function SaveButton() { const isOnline = useOnlineStatus(); function handleSaveClick() { console.log('✅ Progress saved'); } return ( <button disabled={!isOnline} onClick={handleSaveClick}> {isOnline ? 'Save progress' : 'Reconnecting...'} </button> ); } export default function App() { return ( <> <SaveButton /> <StatusBar /> </> ); }
验证打开和关闭网络是否会更新这两个组件。
现在你的组件没有那么多重复的逻辑了。 更重要的是,它们内部的代码描述了它们想要做什么(使用在线状态!),而不是如何做(通过订阅浏览器事件)。
当你将逻辑提取到自定义 Hook 中时,你可以隐藏你如何处理某些外部系统或浏览器 API 的繁琐细节。你的组件代码表达了你的意图,而不是实现。
钩子名称始终以 use
开头。
React 应用程序由组件构成。组件由钩子构成,无论是内置钩子还是自定义钩子。你可能会经常使用其他人创建的自定义钩子,但偶尔你可能也会自己写一个!
你必须遵循以下命名约定
- React 组件名称必须以大写字母开头, 例如
StatusBar
和SaveButton
。React 组件还需要返回 React 知道如何显示的内容,例如一段 JSX。 - 钩子名称必须以
use
开头,后跟一个大写字母, 例如useState
(内置)或useOnlineStatus
(自定义,如本页前面所示)。钩子可以返回任意值。
此约定可确保你始终可以通过查看组件来了解其状态、副作用和其他 React 功能可能“隐藏”的位置。例如,如果你在组件中看到 getColor()
函数调用,则可以确定它内部不可能包含 React 状态,因为其名称不是以 use
开头的。但是,像 useOnlineStatus()
这样的函数调用很可能包含对内部其他钩子的调用!
深入探讨
不。不调用钩子的函数不需要是钩子。
如果你的函数没有调用任何钩子,请避免使用 use
前缀。而是将其编写为没有 use
前缀的常规函数。例如,下面的 useSorted
不调用钩子,因此将其称为 getSorted
// 🔴 Avoid: A Hook that doesn't use Hooks
function useSorted(items) {
return items.slice().sort();
}
// ✅ Good: A regular function that doesn't use Hooks
function getSorted(items) {
return items.slice().sort();
}
这可确保你的代码可以在任何地方调用此常规函数,包括在条件语句中
function List({ items, shouldSort }) {
let displayedItems = items;
if (shouldSort) {
// ✅ It's ok to call getSorted() conditionally because it's not a Hook
displayedItems = getSorted(items);
}
// ...
}
如果函数在内部使用至少一个钩子,则应为其添加 use
前缀(并因此使其成为钩子)。
// ✅ Good: A Hook that uses other Hooks
function useAuth() {
return useContext(Auth);
}
从技术上讲,这不是 React 强制执行的。原则上,你可以创建一个不调用其他钩子的钩子。这通常会造成混淆和限制,因此最好避免这种模式。但是,在极少数情况下,这可能会有所帮助。例如,也许你的函数现在没有使用任何钩子,但你计划将来向其中添加一些钩子调用。那么,使用 use
前缀命名它是有意义的
// ✅ Good: A Hook that will likely use some other Hooks later
function useAuth() {
// TODO: Replace with this line when authentication is implemented:
// return useContext(Auth);
return TEST_USER;
}
然后组件将无法有条件地调用它。当你在其中实际添加钩子调用时,这将变得很重要。如果你不打算在其中使用钩子(现在或以后),请不要将其设为钩子。
自定义钩子允许你共享有状态逻辑,而不是状态本身
在前面的示例中,当你打开和关闭网络时,两个组件会一起更新。但是,认为它们之间共享一个 isOnline
状态变量是错误的。请看以下代码
function StatusBar() {
const isOnline = useOnlineStatus();
// ...
}
function SaveButton() {
const isOnline = useOnlineStatus();
// ...
}
它的工作方式与你在提取重复代码之前相同
function StatusBar() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
// ...
}, []);
// ...
}
function SaveButton() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
// ...
}, []);
// ...
}
这是两个完全独立的状态变量和副作用!它们恰好在同一时间具有相同的值,因为你使用相同的外部值(网络是否打开)同步了它们。
为了更好地说明这一点,我们需要一个不同的示例。请考虑以下 Form
组件
import { useState } from 'react'; export default function Form() { const [firstName, setFirstName] = useState('Mary'); const [lastName, setLastName] = useState('Poppins'); function handleFirstNameChange(e) { setFirstName(e.target.value); } function handleLastNameChange(e) { setLastName(e.target.value); } return ( <> <label> First name: <input value={firstName} onChange={handleFirstNameChange} /> </label> <label> Last name: <input value={lastName} onChange={handleLastNameChange} /> </label> <p><b>Good morning, {firstName} {lastName}.</b></p> </> ); }
每个表单字段都有一些重复的逻辑
- 有一段状态(
firstName
和lastName
)。 - 有一个更改处理程序(
handleFirstNameChange
和handleLastNameChange
)。 - 有一段 JSX 指定了该输入的
value
和onChange
属性。
你可以将重复的逻辑提取到以下 useFormInput
自定义钩子中
import { useState } from 'react'; export function useFormInput(initialValue) { const [value, setValue] = useState(initialValue); function handleChange(e) { setValue(e.target.value); } const inputProps = { value: value, onChange: handleChange }; return inputProps; }
请注意,它仅声明了一个名为 value
的状态变量。
但是,Form
组件调用了两次 useFormInput
:
function Form() {
const firstNameProps = useFormInput('Mary');
const lastNameProps = useFormInput('Poppins');
// ...
这就是为什么它像声明两个独立的状态变量一样工作!
自定义钩子允许你共享有状态逻辑,但不能共享状态本身。对钩子的每次调用都完全独立于对同一钩子的所有其他调用。 这就是为什么上面的两个沙盒完全等效的原因。如果你愿意,可以向上滚动并比较它们。提取自定义钩子之前和之后的行为是相同的。
当你需要在多个组件之间共享状态本身时,请 将其提升并传递下去。
在 Hooks 之间传递响应式值
自定义 Hook 中的代码会在组件每次重新渲染时重新运行。这就是为什么,像组件一样,自定义 Hook 需要是纯函数。 将自定义 Hook 的代码视为组件主体的一部分!
因为自定义 Hook 会与组件一起重新渲染,所以它们总是接收最新的 props 和 state。要了解这意味着什么,请考虑这个聊天室示例。更改服务器 URL 或聊天室
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; import { showNotification } from './notifications.js'; export default function ChatRoom({ roomId }) { const [serverUrl, setServerUrl] = useState('https://127.0.0.1:1234'); useEffect(() => { const options = { serverUrl: serverUrl, roomId: roomId }; const connection = createConnection(options); connection.on('message', (msg) => { showNotification('New message: ' + msg); }); connection.connect(); return () => connection.disconnect(); }, [roomId, serverUrl]); return ( <> <label> Server URL: <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} /> </label> <h1>Welcome to the {roomId} room!</h1> </> ); }
当你更改 serverUrl
或 roomId
时,Effect “响应”你的更改 并重新同步。你可以通过控制台消息判断出,每次你更改 Effect 的依赖项时,聊天都会重新连接。
现在将 Effect 的代码移至自定义 Hook 中
export function useChatRoom({ serverUrl, roomId }) {
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
connection.on('message', (msg) => {
showNotification('New message: ' + msg);
});
return () => connection.disconnect();
}, [roomId, serverUrl]);
}
这使你的 ChatRoom
组件可以调用你的自定义 Hook,而无需担心它在内部是如何工作的
export default function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://127.0.0.1:1234');
useChatRoom({
roomId: roomId,
serverUrl: serverUrl
});
return (
<>
<label>
Server URL:
<input value={serverUrl} onChange={e => setServerUrl(e.target.value)} />
</label>
<h1>Welcome to the {roomId} room!</h1>
</>
);
}
这看起来简单多了!(但它做的事情是一样的。)
请注意,逻辑仍然会响应 props 和 state 的更改。尝试编辑服务器 URL 或所选房间
import { useState } from 'react'; import { useChatRoom } from './useChatRoom.js'; export default function ChatRoom({ roomId }) { const [serverUrl, setServerUrl] = useState('https://127.0.0.1:1234'); useChatRoom({ roomId: roomId, serverUrl: serverUrl }); return ( <> <label> Server URL: <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} /> </label> <h1>Welcome to the {roomId} room!</h1> </> ); }
注意你是如何获取一个 Hook 的返回值的
export default function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://127.0.0.1:1234');
useChatRoom({
roomId: roomId,
serverUrl: serverUrl
});
// ...
并将其作为输入传递给另一个 Hook
export default function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://127.0.0.1:1234');
useChatRoom({
roomId: roomId,
serverUrl: serverUrl
});
// ...
每次你的 ChatRoom
组件重新渲染时,它都会将最新的 roomId
和 serverUrl
传递给你的 Hook。这就是为什么每当在重新渲染后它们的值不同时,你的 Effect 都会重新连接到聊天。(如果你曾经使用过音频或视频处理软件,那么像这样链接 Hook 可能会让你想起链接视觉或音频效果。就好像 useState
的输出“馈入”了 useChatRoom
的输入。)
将事件处理程序传递给自定义 Hook
当你开始在更多组件中使用 useChatRoom
时,你可能希望让组件自定义其行为。例如,目前,在收到消息时要做什么的逻辑硬编码在 Hook 中
export function useChatRoom({ serverUrl, roomId }) {
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
connection.on('message', (msg) => {
showNotification('New message: ' + msg);
});
return () => connection.disconnect();
}, [roomId, serverUrl]);
}
假设你想将此逻辑移回你的组件
export default function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://127.0.0.1:1234');
useChatRoom({
roomId: roomId,
serverUrl: serverUrl,
onReceiveMessage(msg) {
showNotification('New message: ' + msg);
}
});
// ...
为了实现这一点,请更改你的自定义 Hook 以将 onReceiveMessage
作为其命名选项之一
export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) {
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
connection.on('message', (msg) => {
onReceiveMessage(msg);
});
return () => connection.disconnect();
}, [roomId, serverUrl, onReceiveMessage]); // ✅ All dependencies declared
}
这将起作用,但是当你的自定义 Hook 接受事件处理程序时,你还可以进行一项改进。
添加对 onReceiveMessage
的依赖是不理想的,因为它会导致每次组件重新渲染时聊天都重新连接。将此事件处理程序包装到 Effect 事件中以将其从依赖项中删除:
import { useEffect, useEffectEvent } from 'react';
// ...
export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) {
const onMessage = useEffectEvent(onReceiveMessage);
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
connection.on('message', (msg) => {
onMessage(msg);
});
return () => connection.disconnect();
}, [roomId, serverUrl]); // ✅ All dependencies declared
}
现在,每次 ChatRoom
组件重新渲染时,聊天都不会重新连接。这是一个将事件处理程序传递给自定义 Hook 的完整工作演示,你可以尝试一下
import { useState } from 'react'; import { useChatRoom } from './useChatRoom.js'; import { showNotification } from './notifications.js'; export default function ChatRoom({ roomId }) { const [serverUrl, setServerUrl] = useState('https://127.0.0.1:1234'); useChatRoom({ roomId: roomId, serverUrl: serverUrl, onReceiveMessage(msg) { showNotification('New message: ' + msg); } }); return ( <> <label> Server URL: <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} /> </label> <h1>Welcome to the {roomId} room!</h1> </> ); }
注意你不再需要知道 useChatRoom
是如何工作的才能使用它。你可以将它添加到任何其他组件,传递任何其他选项,它的工作方式都相同。这就是自定义 Hook 的强大之处。
何时使用自定义 Hook
你不需要为每个重复的代码段都提取一个自定义 Hook。一些重复是可以的。例如,提取一个 useFormInput
Hook 来包装像前面那样的单个 useState
调用可能是没有必要的。
但是,每当你编写一个 Effect 时,请考虑将其包装在一个自定义 Hook 中是否会更清晰。你不应该经常需要 Effect, 因此,如果你正在编写一个 Effect,这意味着你需要“走出 React”来与某个外部系统同步,或者做一些 React 没有内置 API 的事情。将其包装到自定义 Hook 中可以让你精确地传达你的意图以及数据是如何流经它的。
例如,考虑一个 ShippingForm
组件,它显示两个下拉列表:一个显示城市列表,另一个显示所选城市的地区列表。你可能会从一些看起来像这样的代码开始
function ShippingForm({ country }) {
const [cities, setCities] = useState(null);
// This Effect fetches cities for a country
useEffect(() => {
let ignore = false;
fetch(`/api/cities?country=${country}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setCities(json);
}
});
return () => {
ignore = true;
};
}, [country]);
const [city, setCity] = useState(null);
const [areas, setAreas] = useState(null);
// This Effect fetches areas for the selected city
useEffect(() => {
if (city) {
let ignore = false;
fetch(`/api/areas?city=${city}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setAreas(json);
}
});
return () => {
ignore = true;
};
}
}, [city]);
// ...
虽然这段代码相当重复,但将这些 Effect 相互分离是正确的。 它们同步不同的东西,所以你不应该将它们合并到一个 Effect 中。相反,你可以通过将它们之间的通用逻辑提取到你自己的 useData
Hook 中来简化上面的 ShippingForm
组件
function useData(url) {
const [data, setData] = useState(null);
useEffect(() => {
if (url) {
let ignore = false;
fetch(url)
.then(response => response.json())
.then(json => {
if (!ignore) {
setData(json);
}
});
return () => {
ignore = true;
};
}
}, [url]);
return data;
}
现在,你可以使用对 useData
的调用来替换 ShippingForm
组件中的两个 Effect
function ShippingForm({ country }) {
const cities = useData(`/api/cities?country=${country}`);
const [city, setCity] = useState(null);
const areas = useData(city ? `/api/areas?city=${city}` : null);
// ...
提取自定义 Hook 使数据流变得清晰。你传入 url
并获得 data
。通过将你的 Effect “隐藏”在 useData
中,你还可以防止在 ShippingForm
组件上工作的人向其中添加不必要的依赖项。随着时间的推移,你的应用程序中的大多数 Effect 都将位于自定义 Hook 中。
深入探讨
首先,选择你的自定义 Hook 的名称。如果你难以选择一个清晰的名称,这可能意味着你的 Effect 与组件的其余逻辑过于耦合,并且尚未准备好被提取。
理想情况下,你的自定义 Hook 的名称应该足够清晰,即使是不经常编写代码的人也能很好地猜测出你的自定义 Hook 的作用、接受的参数以及返回的内容。
- ✅
useData(url)
- ✅
useImpressionLog(eventName, extraData)
- ✅
useChatRoom(options)
当你与外部系统同步时,你的自定义 Hook 名称可能更具技术性,并使用特定于该系统的术语。只要熟悉该系统的人能够理解,这就是可以的。
- ✅
useMediaQuery(query)
- ✅
useSocket(url)
- ✅
useIntersectionObserver(ref, options)
让自定义 Hook 专注于具体的、高阶用例。避免创建和使用自定义的“生命周期”Hook,这些 Hook 作为 useEffect
API 本身的替代方案和便捷包装器。
- 🔴
useMount(fn)
- 🔴
useEffectOnce(fn)
- 🔴
useUpdateEffect(fn)
例如,这个 useMount
Hook 试图确保某些代码只在“挂载时”运行。
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://127.0.0.1:1234');
// 🔴 Avoid: using custom "lifecycle" Hooks
useMount(() => {
const connection = createConnection({ roomId, serverUrl });
connection.connect();
post('/analytics/event', { eventName: 'visit_chat' });
});
// ...
}
// 🔴 Avoid: creating custom "lifecycle" Hooks
function useMount(fn) {
useEffect(() => {
fn();
}, []); // 🔴 React Hook useEffect has a missing dependency: 'fn'
}
像 useMount
这样的自定义“生命周期”Hook 不太适合 React 的范例。 例如,这个代码示例中有一个错误(它没有对 roomId
或 serverUrl
的变化做出“反应”),但 linter 不会警告你,因为它只检查直接的 useEffect
调用。它不会知道你的 Hook。
如果你正在编写 Effect,请先直接使用 React API。
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://127.0.0.1:1234');
// ✅ Good: two raw Effects separated by purpose
useEffect(() => {
const connection = createConnection({ serverUrl, roomId });
connection.connect();
return () => connection.disconnect();
}, [serverUrl, roomId]);
useEffect(() => {
post('/analytics/event', { eventName: 'visit_chat', roomId });
}, [roomId]);
// ...
}
然后,你可以(但不是必须)为不同的高阶用例提取自定义 Hook。
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://127.0.0.1:1234');
// ✅ Great: custom Hooks named after their purpose
useChatRoom({ serverUrl, roomId });
useImpressionLog('visit_chat', { roomId });
// ...
}
一个好的自定义 Hook 通过限制其功能使调用代码更具声明性。例如,useChatRoom(options)
只能连接到聊天室,而 useImpressionLog(eventName, extraData)
只能向分析系统发送展示日志。如果你的自定义 Hook API 没有限制用例并且非常抽象,那么从长远来看,它很可能弊大于利。
自定义 Hook 帮助你迁移到更好的模式
Effect 是一个 “逃生舱口”:当你需要“跳出 React”并且没有更好的内置解决方案适用于你的用例时,你可以使用它们。随着时间的推移,React 团队的目标是通过为更具体的问题提供更具体的解决方案,将应用程序中 Effect 的数量减少到最低限度。将你的 Effect 封装在自定义 Hook 中,可以在这些解决方案可用时更容易地升级你的代码。
让我们回到这个例子
import { useState, useEffect } from 'react'; export function useOnlineStatus() { const [isOnline, setIsOnline] = useState(true); useEffect(() => { function handleOnline() { setIsOnline(true); } function handleOffline() { setIsOnline(false); } window.addEventListener('online', handleOnline); window.addEventListener('offline', handleOffline); return () => { window.removeEventListener('online', handleOnline); window.removeEventListener('offline', handleOffline); }; }, []); return isOnline; }
在上面的例子中,useOnlineStatus
是用一对 useState
和 useEffect
实现的。然而,这不是最好的解决方案。它没有考虑到许多边缘情况。例如,它假设当组件挂载时,isOnline
已经是 true
,但如果网络已经离线,这可能是错误的。你可以使用浏览器 navigator.onLine
API 来检查这一点,但直接使用它在服务器上生成初始 HTML 时不起作用。简而言之,这段代码可以改进。
幸运的是,React 18 包含一个名为 useSyncExternalStore
的专用 API,它可以为你解决所有这些问题。下面是你的 useOnlineStatus
Hook,重写后利用了这个新 API。
import { useSyncExternalStore } from 'react'; function subscribe(callback) { window.addEventListener('online', callback); window.addEventListener('offline', callback); return () => { window.removeEventListener('online', callback); window.removeEventListener('offline', callback); }; } export function useOnlineStatus() { return useSyncExternalStore( subscribe, () => navigator.onLine, // How to get the value on the client () => true // How to get the value on the server ); }
注意 你不需要改变任何组件 来进行这次迁移。
function StatusBar() {
const isOnline = useOnlineStatus();
// ...
}
function SaveButton() {
const isOnline = useOnlineStatus();
// ...
}
这是为什么将 Effect 封装在自定义 Hook 中通常是有益的另一个原因。
- 你可以使进出 Effect 的数据流非常明确。
- 你可以让你的组件专注于意图,而不是专注于 Effect 的确切实现。
- 当 React 添加新功能时,你可以删除那些 Effect,而无需更改任何组件。
类似于 设计系统,你可能会发现将应用程序组件中的常见习惯用法提取到自定义 Hook 中很有帮助。这将使你的组件代码专注于意图,并让你避免经常编写原始的 Effect。React 社区维护着许多优秀的自定义 Hook。
深入探讨
我们仍在研究细节,但我们预计在未来,你将像这样编写数据获取代码。
import { use } from 'react'; // Not available yet!
function ShippingForm({ country }) {
const cities = use(fetch(`/api/cities?country=${country}`));
const [city, setCity] = useState(null);
const areas = city ? use(fetch(`/api/areas?city=${city}`)) : null;
// ...
如果你在应用程序中使用了像上面的 useData
这样的自定义 Hook,那么与在每个组件中手动编写原始 Effect 相比,迁移到最终推荐的方法所需的变化更少。然而,旧的方法仍然可以正常工作,所以如果你觉得编写原始 Effect 很舒服,你可以继续这样做。
条条大路通罗马
假设你想使用浏览器 requestAnimationFrame
API 从头开始 实现淡入动画。 你可以从一个设置动画循环的 Effect 开始。 在动画的每一帧中,你可以更改 使用 ref 保存的 DOM 节点的透明度,直到它达到 1
。 你的代码可能像这样开始
import { useState, useEffect, useRef } from 'react'; function Welcome() { const ref = useRef(null); useEffect(() => { const duration = 1000; const node = ref.current; let startTime = performance.now(); let frameId = null; function onFrame(now) { const timePassed = now - startTime; const progress = Math.min(timePassed / duration, 1); onProgress(progress); if (progress < 1) { // We still have more frames to paint frameId = requestAnimationFrame(onFrame); } } function onProgress(progress) { node.style.opacity = progress; } function start() { onProgress(0); startTime = performance.now(); frameId = requestAnimationFrame(onFrame); } function stop() { cancelAnimationFrame(frameId); startTime = null; frameId = null; } start(); return () => stop(); }, []); return ( <h1 className="welcome" ref={ref}> Welcome </h1> ); } export default function App() { const [show, setShow] = useState(false); return ( <> <button onClick={() => setShow(!show)}> {show ? 'Remove' : 'Show'} </button> <hr /> {show && <Welcome />} </> ); }
为了使组件更具可读性,你可以将逻辑提取到自定义 Hook useFadeIn
中
import { useState, useEffect, useRef } from 'react'; import { useFadeIn } from './useFadeIn.js'; function Welcome() { const ref = useRef(null); useFadeIn(ref, 1000); return ( <h1 className="welcome" ref={ref}> Welcome </h1> ); } export default function App() { const [show, setShow] = useState(false); return ( <> <button onClick={() => setShow(!show)}> {show ? 'Remove' : 'Show'} </button> <hr /> {show && <Welcome />} </> ); }
你可以保持 useFadeIn
代码不变,但你也可以对其进行更多重构。 例如,你可以将设置动画循环的逻辑从 useFadeIn
中提取到自定义 Hook useAnimationLoop
中
import { useState, useEffect } from 'react'; import { experimental_useEffectEvent as useEffectEvent } from 'react'; export function useFadeIn(ref, duration) { const [isRunning, setIsRunning] = useState(true); useAnimationLoop(isRunning, (timePassed) => { const progress = Math.min(timePassed / duration, 1); ref.current.style.opacity = progress; if (progress === 1) { setIsRunning(false); } }); } function useAnimationLoop(isRunning, drawFrame) { const onFrame = useEffectEvent(drawFrame); useEffect(() => { if (!isRunning) { return; } const startTime = performance.now(); let frameId = null; function tick(now) { const timePassed = now - startTime; onFrame(timePassed); frameId = requestAnimationFrame(tick); } tick(); return () => cancelAnimationFrame(frameId); }, [isRunning]); }
但是,你并非必须这样做。 与常规函数一样,你最终决定在代码的不同部分之间划定界限。 你也可以采用一种截然不同的方法。 你可以将大部分命令式逻辑移动到 JavaScript 类 中,而不是将其保留在 Effect 中:
import { useState, useEffect } from 'react'; import { FadeInAnimation } from './animation.js'; export function useFadeIn(ref, duration) { useEffect(() => { const animation = new FadeInAnimation(ref.current); animation.start(duration); return () => { animation.stop(); }; }, [ref, duration]); }
Effects 允许你将 React 连接到外部系统。 Effect 之间需要的协调越多(例如,链接多个动画),就越有必要像上面的沙盒一样将该逻辑完全从 Effect 和 Hook 中提取出来。 然后,你提取的代码变成了“外部系统”。 这使得你的 Effect 保持简单,因为它们只需要向你移到 React 之外的系统发送消息。
上面的例子假设淡入逻辑需要用 JavaScript 编写。 但是,这个特定的淡入动画可以使用简单的 CSS 动画 更简单、更高效地实现:
.welcome { color: white; padding: 50px; text-align: center; font-size: 50px; background-image: radial-gradient(circle, rgba(63,94,251,1) 0%, rgba(252,70,107,1) 100%); animation: fadeIn 1000ms; } @keyframes fadeIn { 0% { opacity: 0; } 100% { opacity: 1; } }
有时,你甚至不需要 Hook!
回顾
- 自定义 Hook 允许你在组件之间共享逻辑。
- 自定义 Hook 的名称必须以
use
开头,后跟一个大写字母。 - 自定义 Hook 仅共享有状态逻辑,而不共享状态本身。
- 你可以在 Hook 之间传递响应式值,并且它们会保持最新状态。
- 每次组件重新渲染时,所有 Hook 都会重新运行。
- 你的自定义 Hook 的代码应该是纯净的,就像你的组件代码一样。
- 将自定义 Hook 接收到的事件处理程序包装到 Effect 事件中。
- 不要创建像
useMount
这样的自定义 Hook。 保持它们的用途特定。 - 如何以及在哪里选择代码的边界取决于你。
挑战 1共 5: 提取 useCounter
Hook
此组件使用状态变量和 Effect 来显示每秒递增的数字。 将此逻辑提取到名为 useCounter
的自定义 Hook 中。 你的目标是使 Counter
组件的实现看起来像这样
export default function Counter() {
const count = useCounter();
return <h1>Seconds passed: {count}</h1>;
}
你需要在 useCounter.js
中编写你的自定义 Hook,并将其导入到 App.js
文件中。
import { useState, useEffect } from 'react'; export default function Counter() { const [count, setCount] = useState(0); useEffect(() => { const id = setInterval(() => { setCount(c => c + 1); }, 1000); return () => clearInterval(id); }, []); return <h1>Seconds passed: {count}</h1>; }