你的一些组件可能需要控制并与 React 系统外部的系统同步。例如,你可能需要使用浏览器 API 来聚焦输入,播放和暂停使用非 React 实现的视频播放器,或连接并监听来自远程服务器的消息。在本章中,你将学习能够让你“跳出”React 并连接到外部系统的逃生舱口。你的大部分应用程序逻辑和数据流不应依赖于这些功能。
本章内容
使用 refs 引用值
当你想让组件“记住”一些信息,但又不想让这些信息触发新的渲染时,可以使用ref
const ref = useRef(0);
与状态一样,refs 在重新渲染之间由 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 不会追踪它。例如,你可以使用 refs 来存储超时 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
prop 值保持同步
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 一次。这就是你看到"✅ Connecting..."
打印两次的原因。这确保你不会忘记实现清理函数。
你可能不需要 useEffect
useEffect 是 React 范式的一种规避方法。它允许你“跳出”React 并将你的组件与某些外部系统同步。如果没有涉及外部系统(例如,如果只想在某些 props 或 state 更改时更新组件的状态),则不需要 useEffect。移除不必要的 useEffect 将使你的代码更易于理解、运行速度更快且更不容易出错。
有两种常见情况你不需要使用 Effects
- 你不需要 Effects 来转换数据以进行渲染。
- 你不需要 Effects 来处理用户事件。
例如,你不需要 useEffect 来根据其他状态调整某些状态
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 来与外部系统同步。
响应式 effect 的生命周期
Effects 的生命周期与组件不同。组件可以挂载、更新或卸载。一个 Effect 只做两件事:开始同步某些东西,然后停止同步它。如果你的 Effect 依赖于随时间变化的 props 和 state,则此周期可以发生多次。
此 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 分离
事件处理程序仅在再次执行相同的交互操作时才会重新运行。与事件处理程序不同,如果 Effect 读取的任何值(例如 props 或 state)与上次渲染时不同,则 Effects 会重新同步。有时,你希望同时拥有两种行为:一个响应某些值但并非所有值的 Effect。
Effects 内部的所有代码都是响应式的。如果它读取的某些响应式值由于重新渲染而发生了更改,它将再次运行。例如,如果roomId
或theme
发生了更改,则此 Effect 将重新连接到聊天。
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
的代码从你的 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 依赖项列表中包含 Effect 读取的每个响应式值(例如 props 和 state)。这确保你的 Effect 与组件的最新 props 和 state 保持同步。不必要的依赖项可能会导致你的 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 代码使用的所有响应式值的列表。你不会故意选择要放入该列表中的内容。该列表描述你的代码。要更改依赖项列表,请更改代码。
使用自定义 Hook 重复使用逻辑
React 自带内置 Hook,例如 useState
、useContext
和 useEffect
。有时,你会希望有一个 Hook 用于更具体的用途:例如,获取数据、跟踪用户是否在线或连接到聊天室。为此,您可以根据应用程序的需要创建自己的 Hook。
在这个例子中,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, }} /> ); }
您可以创建自定义 Hook,将它们组合在一起,在它们之间传递数据,并在组件之间重复使用它们。随着应用程序的增长,您将编写更少的 Effect,因为您可以重复使用已经编写的自定义 Hook。React 社区也维护着许多优秀的自定义 Hook。
下一步是什么?
前往 使用 refs 引用值 开始逐页阅读本章节!