一些组件可能需要控制和同步 React 外部的系统。例如,你可能需要使用浏览器 API 聚焦一个输入框,播放和暂停一个非 React 实现的视频播放器,或者连接到远程服务器并监听消息。在本章中,你将学习一些“逃生舱口”,这些舱口允许你“走出” React 并连接到外部系统。你的大多数应用程序逻辑和数据流不应该依赖于这些功能。
本章内容
使用 Refs 引用值
当你希望一个组件“记住”一些信息,但又不想让这些信息触发新的渲染,你可以使用一个ref
const ref = useRef(0);
与状态类似,ref 在 React 的多次渲染之间都会保留。但是,设置状态会重新渲染组件。更改 ref 不会!你可以通过ref.current
属性访问该 ref 的当前值。
import { useRef } from 'react'; export default function Counter() { let ref = useRef(0); function handleClick() { ref.current = ref.current + 1; alert('You clicked ' + ref.current + ' times!'); } return ( <button onClick={handleClick}> Click me! </button> ); }
Ref 就像你组件的秘密口袋,React 不会跟踪它。例如,你可以使用 ref 来存储超时 ID、DOM 元素和其他不会影响组件渲染输出的对象。
使用 Refs 操作 DOM
React 会自动更新 DOM 以匹配你的渲染输出,因此你的组件通常不需要操作它。但是,有时你可能需要访问 React 管理的 DOM 元素——例如,聚焦一个节点、滚动到它,或者测量它的尺寸和位置。在 React 中没有内置的方法来执行这些操作,因此你需要一个指向 DOM 节点的 ref。例如,点击按钮将使用 ref 聚焦输入框。
import { useRef } from 'react'; export default function Form() { const inputRef = useRef(null); function handleClick() { inputRef.current.focus(); } return ( <> <input ref={inputRef} /> <button onClick={handleClick}> Focus the input </button> </> ); }
与 Effects 同步
一些组件需要与外部系统同步。例如,你可能希望根据 React 状态控制一个非 React 组件,建立一个服务器连接,或者在组件出现在屏幕上时发送一个分析日志。与事件处理程序不同(事件处理程序允许你处理特定的事件),Effects 允许你渲染后运行一些代码。使用它们将你的组件与 React 外部的系统同步。
按播放/暂停几次,看看视频播放器如何始终与isPlaying
属性值同步。
import { useState, useRef, useEffect } from 'react'; function VideoPlayer({ src, isPlaying }) { const ref = useRef(null); useEffect(() => { if (isPlaying) { ref.current.play(); } else { ref.current.pause(); } }, [isPlaying]); return <video ref={ref} src={src} loop playsInline />; } export default function App() { const [isPlaying, setIsPlaying] = useState(false); return ( <> <button onClick={() => setIsPlaying(!isPlaying)}> {isPlaying ? 'Pause' : 'Play'} </button> <VideoPlayer isPlaying={isPlaying} src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4" /> </> ); }
许多 Effects 还会“清理”自身。例如,一个建立与聊天服务器连接的 Effect 应该返回一个清理函数,告诉 React 如何将你的组件从该服务器断开连接。
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; export default function ChatRoom() { useEffect(() => { const connection = createConnection(); connection.connect(); return () => connection.disconnect(); }, []); return <h1>Welcome to the chat!</h1>; }
在开发中,React 会立即运行并额外清理你的 Effect 一次。这就是为什么你看到"✅ 连接中..."
打印两次。这确保了你不会忘记实现清理函数。
你可能不需要 Effect
Effects 是 React 范式的一种“逃生舱口”。它们允许你“走出” React 并将你的组件与某个外部系统同步。如果没有涉及外部系统(例如,如果你希望在某些属性或状态发生变化时更新组件的状态),你就不需要 Effect。删除不必要的 Effects 将使你的代码更容易理解、运行更快、错误更少。
有两种常见的情况,你不需要使用 Effects
- 你不必使用 Effects 来转换用于渲染的数据。
- 你不必使用 Effects 来处理用户事件。
例如,你不必使用 Effect 来根据其他状态调整某个状态
function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// 🔴 Avoid: redundant state and unnecessary Effect
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
// ...
}
相反,在渲染时尽可能多地进行计算。
function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// ✅ Good: calculated during rendering
const fullName = firstName + ' ' + lastName;
// ...
}
但是,您 *确实* 需要 Effects 与外部系统同步。
反应式 Effects 的生命周期
Effects 具有与组件不同的生命周期。组件可以挂载、更新或卸载。Effect 只能做两件事:开始同步某件事,然后停止同步它。如果您的 Effect 依赖于随时间变化的 props 和状态,此循环可能会发生多次。
此 Effect 依赖于 roomId
prop 的值。Props 是 *反应式值*,这意味着它们可以在重新渲染时发生变化。请注意,如果 roomId
发生变化,Effect 会 *重新同步*(并重新连接到服务器)。
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; const serverUrl = 'https://127.0.0.1:1234'; function ChatRoom({ roomId }) { useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.connect(); return () => connection.disconnect(); }, [roomId]); return <h1>Welcome to the {roomId} room!</h1>; } export default function App() { const [roomId, setRoomId] = useState('general'); return ( <> <label> Choose the chat room:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">general</option> <option value="travel">travel</option> <option value="music">music</option> </select> </label> <hr /> <ChatRoom roomId={roomId} /> </> ); }
React 提供了一个 linter 规则来检查您是否正确指定了 Effect 的依赖项。如果您忘记在上面的示例中将 roomId
指定在依赖项列表中,linter 会自动找到该错误。
将事件与 Effects 分开
事件处理程序仅在您再次执行相同的交互时才重新运行。与事件处理程序不同,如果 Effects 读取的任何值(如 props 或状态)与上次渲染时不同,Effects 会重新同步。有时,您希望同时拥有两种行为:Effect 会响应某些值但不会响应其他值的重新运行。
Effects 中的所有代码都是 *反应式* 的。如果它读取的某个反应式值由于重新渲染而发生了变化,它将再次运行。例如,此 Effect 将重新连接到聊天,前提是 roomId
或 theme
发生了变化
import { useState, useEffect } from 'react'; import { createConnection, sendMessage } from './chat.js'; import { showNotification } from './notifications.js'; const serverUrl = 'https://127.0.0.1:1234'; function ChatRoom({ roomId, theme }) { useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.on('connected', () => { showNotification('Connected!', theme); }); connection.connect(); return () => connection.disconnect(); }, [roomId, theme]); return <h1>Welcome to the {roomId} room!</h1> } export default function App() { const [roomId, setRoomId] = useState('general'); const [isDark, setIsDark] = useState(false); return ( <> <label> Choose the chat room:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">general</option> <option value="travel">travel</option> <option value="music">music</option> </select> </label> <label> <input type="checkbox" checked={isDark} onChange={e => setIsDark(e.target.checked)} /> Use dark theme </label> <hr /> <ChatRoom roomId={roomId} theme={isDark ? 'dark' : 'light'} /> </> ); }
这不是理想的。您只想在 roomId
发生变化时重新连接到聊天。切换 theme
不应该重新连接到聊天!将读取 theme
的代码从您的 Effect 中移到 *Effect 事件* 中。
import { useState, useEffect } from 'react'; import { experimental_useEffectEvent as useEffectEvent } from 'react'; import { createConnection, sendMessage } from './chat.js'; import { showNotification } from './notifications.js'; const serverUrl = 'https://127.0.0.1:1234'; function ChatRoom({ roomId, theme }) { const onConnected = useEffectEvent(() => { showNotification('Connected!', theme); }); useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.on('connected', () => { onConnected(); }); connection.connect(); return () => connection.disconnect(); }, [roomId]); return <h1>Welcome to the {roomId} room!</h1> } export default function App() { const [roomId, setRoomId] = useState('general'); const [isDark, setIsDark] = useState(false); return ( <> <label> Choose the chat room:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">general</option> <option value="travel">travel</option> <option value="music">music</option> </select> </label> <label> <input type="checkbox" checked={isDark} onChange={e => setIsDark(e.target.checked)} /> Use dark theme </label> <hr /> <ChatRoom roomId={roomId} theme={isDark ? 'dark' : 'light'} /> </> ); }
Effect 事件中的代码不是反应式代码,因此更改 theme
将不再使您的 Effect 重新连接。
删除 Effect 依赖项
当您编写 Effect 时,linter 会验证您是否已将 Effect 读取的每个反应式值(如 props 和状态)都包含在 Effect 依赖项列表中。这可确保您的 Effect 与组件的最新 props 和状态保持同步。不必要的依赖项可能会导致您的 Effect 过于频繁地运行,甚至会造成无限循环。删除它们的方式取决于具体情况。
例如,此 Effect 依赖于 options
对象,该对象在您每次编辑输入时都会重新创建。
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; const serverUrl = 'https://127.0.0.1:1234'; function ChatRoom({ roomId }) { const [message, setMessage] = useState(''); const options = { serverUrl: serverUrl, roomId: roomId }; useEffect(() => { const connection = createConnection(options); connection.connect(); return () => connection.disconnect(); }, [options]); return ( <> <h1>Welcome to the {roomId} room!</h1> <input value={message} onChange={e => setMessage(e.target.value)} /> </> ); } export default function App() { const [roomId, setRoomId] = useState('general'); return ( <> <label> Choose the chat room:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">general</option> <option value="travel">travel</option> <option value="music">music</option> </select> </label> <hr /> <ChatRoom roomId={roomId} /> </> ); }
您不希望聊天在您每次开始在该聊天中键入消息时都重新连接。要解决此问题,请将 options
对象的创建移到 Effect 内部,以便 Effect 仅依赖于 roomId
字符串。
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; const serverUrl = 'https://127.0.0.1:1234'; function ChatRoom({ roomId }) { const [message, setMessage] = useState(''); useEffect(() => { const options = { serverUrl: serverUrl, roomId: roomId }; const connection = createConnection(options); connection.connect(); return () => connection.disconnect(); }, [roomId]); return ( <> <h1>Welcome to the {roomId} room!</h1> <input value={message} onChange={e => setMessage(e.target.value)} /> </> ); } export default function App() { const [roomId, setRoomId] = useState('general'); return ( <> <label> Choose the chat room:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">general</option> <option value="travel">travel</option> <option value="music">music</option> </select> </label> <hr /> <ChatRoom roomId={roomId} /> </> ); }
请注意,您没有一开始就编辑依赖项列表以删除 options
依赖项。那将是错误的。相反,您更改了周围的代码,以便依赖项变得 *不再需要*。将依赖项列表视为 Effect 代码使用的所有反应式值的列表。您不会有意选择要放在该列表中的内容。该列表描述了您的代码。要更改依赖项列表,请更改代码。
使用自定义 Hooks 重用逻辑
React 带有内置的 Hooks,如 useState
、useContext
和 useEffect
。有时,您会希望有一个用于更特定目的的 Hook:例如,用于获取数据、跟踪用户是否在线或连接到聊天室。为此,您可以根据应用程序的需要创建自己的 Hooks。
在此示例中,usePointerPosition
自定义 Hook 跟踪光标位置,而 useDelayedValue
自定义 Hook 返回一个值,该值在您传递的值“落后”一定毫秒。将光标移到沙盒预览区域,以查看跟随光标的移动点迹。
import { usePointerPosition } from './usePointerPosition.js'; import { useDelayedValue } from './useDelayedValue.js'; export default function Canvas() { const pos1 = usePointerPosition(); const pos2 = useDelayedValue(pos1, 100); const pos3 = useDelayedValue(pos2, 200); const pos4 = useDelayedValue(pos3, 100); const pos5 = useDelayedValue(pos4, 50); return ( <> <Dot position={pos1} opacity={1} /> <Dot position={pos2} opacity={0.8} /> <Dot position={pos3} opacity={0.6} /> <Dot position={pos4} opacity={0.4} /> <Dot position={pos5} opacity={0.2} /> </> ); } function Dot({ position, opacity }) { return ( <div style={{ position: 'absolute', backgroundColor: 'pink', borderRadius: '50%', opacity, transform: `translate(${position.x}px, ${position.y}px)`, pointerEvents: 'none', left: -20, top: -20, width: 40, height: 40, }} /> ); }
您可以创建自定义 Hooks、将它们组合在一起、在它们之间传递数据并在组件之间重用它们。随着应用程序的增长,您将减少手动编写 Effects,因为您可以重用已编写的自定义 Hooks。React 社区还维护着许多出色的自定义 Hooks。
下一步?
前往 使用 Refs 引用值,逐页阅读本章节内容!