你的一些组件可能需要控制和同步 React 之外的系统。例如,你可能需要使用浏览器 API 聚焦输入,播放和暂停未使用 React 实现的视频播放器,或者连接并收听来自远程服务器的消息。在本章中,你将学习让你 “走出” React 并连接到外部系统的应急方案。大多数应用逻辑和数据流不应依赖这些功能。
¥Some of your components may need to control and synchronize with systems outside of React. For example, you might need to focus an input using the browser API, play and pause a video player implemented without React, or connect and listen to messages from a remote server. In this chapter, you’ll learn the escape hatches that let you “step outside” React and connect to external systems. Most of your application logic and data flow should not rely on these features.
在此章节中
使用引用引用值
¥Referencing values with refs
当你想要一个组件给 “记住” 一些信息,但你不想让这些信息 触发新渲染,你可以使用一个引用:
¥When you want a component to “remember” some information, but you don’t want that information to trigger new renders, you can use a ref:
const ref = useRef(0);
与状态一样,引用在重新渲染之间由 React 保留。但是,设置状态会重新渲染组件。更改引用不会!你可以通过 ref.current
属性访问该引用的当前值。
¥Like state, refs are retained by React between re-renders. However, setting state re-renders a component. Changing a ref does not! You can access the current value of that ref through the ref.current
property.
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> ); }
引用就像 React 不跟踪的组件的秘密口袋。例如,你可以使用引用来存储 超时 ID、DOM 元素 和其他不影响组件渲染输出的对象。
¥A ref is like a secret pocket of your component that React doesn’t track. For example, you can use refs to store timeout IDs, DOM elements, and other objects that don’t impact the component’s rendering output.
准备好学习这个主题了吗?
阅读 使用引用引用值 以了解如何使用引用来记住信息。
¥Read Referencing Values with Refs to learn how to use refs to remember information.
阅读更多使用引用操作 DOM
¥Manipulating the DOM with refs
React 会自动更新 DOM 以匹配你的渲染输出,因此你的组件不需要经常操作它。但是,有时你可能需要访问由 React 管理的 DOM 元素,例如,聚焦节点、滚动到它或测量其大小和位置。在 React 中没有内置的方法来做这些事情,所以你需要一个 DOM 节点的引用。例如,单击按钮将使用引用聚焦输入:
¥React automatically updates the DOM to match your render output, so your components won’t often need to manipulate it. However, sometimes you might need access to the DOM elements managed by React—for example, to focus a node, scroll to it, or measure its size and position. There is no built-in way to do those things in React, so you will need a ref to the DOM node. For example, clicking the button will focus the input using a 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> </> ); }
准备好学习这个主题了吗?
阅读 使用引用操作 DOM 以了解如何访问 React 管理的 DOM 元素。
¥Read Manipulating the DOM with Refs to learn how to access DOM elements managed by React.
阅读更多使用副作用进行同步
¥Synchronizing with Effects
一些组件需要与外部系统同步。例如,你可能希望根据 React 状态控制非 React 组件、设置服务器连接或在组件出现在屏幕上时发送分析日志。与让你处理特定事件的事件处理程序不同,副作用让你在渲染后运行一些代码。使用它们将你的组件与 React 之外的系统同步。
¥Some components need to synchronize with external systems. For example, you might want to control a non-React component based on the React state, set up a server connection, or send an analytics log when a component appears on the screen. Unlike event handlers, which let you handle particular events, Effects let you run some code after rendering. Use them to synchronize your component with a system outside of React.
按播放/暂停几次,看看视频播放器如何与 isPlaying
属性值保持同步:
¥Press Play/Pause a few times and see how the video player stays synchronized to the isPlaying
prop value:
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" /> </> ); }
许多副作用也在它们自己之后 “清理”。例如,一个建立到聊天服务器连接的副作用应该返回一个清理函数,告诉 React 如何断开你的组件与该服务器的连接:
¥Many Effects also “clean up” after themselves. For example, an Effect that sets up a connection to a chat server should return a cleanup function that tells React how to disconnect your component from that server:
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 将立即运行并额外清理一次副作用。这就是你看到 "✅ Connecting..."
打印两次的原因。这确保你不会忘记实现清理函数。
¥In development, React will immediately run and clean up your Effect one extra time. This is why you see "✅ Connecting..."
printed twice. This ensures that you don’t forget to implement the cleanup function.
准备好学习这个主题了吗?
阅读 使用副作用进行同步 以了解如何将组件与外部系统同步。
¥Read Synchronizing with Effects to learn how to synchronize components with external systems.
阅读更多你可能不需要副作用
¥You Might Not Need An Effect
副作用是 React 范式的应急方案。它们让你体验 React 的 “走出”,并让你的组件与一些外部系统同步。如果不涉及外部系统(例如,如果你想在某些属性或状态更改时更新组件的状态),则不需要副作用。删除不必要的副作用将使你的代码更易于理解、运行速度更快并且更不容易出错。
¥Effects are an escape hatch from the React paradigm. They let you “step outside” of React and synchronize your components with some external system. If there is no external system involved (for example, if you want to update a component’s state when some props or state change), you shouldn’t need an Effect. Removing unnecessary Effects will make your code easier to follow, faster to run, and less error-prone.
有两种常见情况不需要副作用:
¥There are two common cases in which you don’t need Effects:
-
你不需要副作用来转换数据以进行渲染。
¥You don’t need Effects to transform data for rendering.
-
你不需要副作用来处理用户事件。
¥You don’t need Effects to handle user events.
例如,你不需要副作用来根据其他状态调整某些状态:
¥For example, you don’t need an Effect to adjust some state based on other state:
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]);
// ...
}
而是,在渲染时尽可能多地计算:
¥Instead, calculate as much as you can while rendering:
function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// ✅ Good: calculated during rendering
const fullName = firstName + ' ' + lastName;
// ...
}
但是,你确实需要副作用才能与外部系统同步。
¥However, you do need Effects to synchronize with external systems.
准备好学习这个主题了吗?
阅读 你可能不需要副作用 以了解如何删除不必要的副作用。
¥Read You Might Not Need an Effect to learn how to remove unnecessary Effects.
阅读更多反应式副作用的生命周期
¥Lifecycle of reactive effects
副作用与组件有不同的生命周期。组件可以挂载、更新或卸载。副作用只能做两件事:开始同步某些东西,然后停止同步。如果你的副作用依赖于随时间变化的属性和状态,这个循环可能会发生多次。
¥Effects have a different lifecycle from components. Components may mount, update, or unmount. An Effect can only do two things: to start synchronizing something, and later to stop synchronizing it. This cycle can happen multiple times if your Effect depends on props and state that change over time.
此副作用取决于 roomId
属性的值。属性是反应值,这意味着它们可以在重新渲染时改变。请注意,如果 roomId
更改,副作用会重新同步(并重新连接到服务器):
¥This Effect depends on the value of the roomId
prop. Props are reactive values, which means they can change on a re-render. Notice that the Effect re-synchronizes (and re-connects to the server) if roomId
changes:
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; const serverUrl = 'https://localhost: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 规则来检查你是否正确指定了副作用的依赖。如果你忘记在上述示例的依赖列表中指定 roomId
,linter 将自动找到该错误。
¥React provides a linter rule to check that you’ve specified your Effect’s dependencies correctly. If you forget to specify roomId
in the list of dependencies in the above example, the linter will find that bug automatically.
准备好学习这个主题了吗?
阅读 反应式事件的生命周期 以了解副作用的生命周期与组件的生命周期有何不同。
¥Read Lifecycle of Reactive Events to learn how an Effect’s lifecycle is different from a component’s.
阅读更多将事件与副作用分开
¥Separating events from Effects
事件处理程序仅在你再次执行相同的交互时重新运行。与事件处理程序不同,如果副作用读取的任何值(如属性或状态)与上次渲染期间不同,则会重新同步。有时,你需要两种行为的混合:响应某些值而不是其他值而重新运行的副作用。
¥Event handlers only re-run when you perform the same interaction again. Unlike event handlers, Effects re-synchronize if any of the values they read, like props or state, are different than during last render. Sometimes, you want a mix of both behaviors: an Effect that re-runs in response to some values but not others.
副作用内的所有代码都是响应式的。如果它读取的某些反应值由于重新渲染而发生变化,它将再次运行。例如,如果 roomId
或 theme
发生变化,此副作用将重新连接到聊天:
¥All code inside Effects is reactive. It will run again if some reactive value it reads has changed due to a re-render. For example, this Effect will re-connect to the chat if either roomId
or theme
have changed:
import { useState, useEffect } from 'react'; import { createConnection, sendMessage } from './chat.js'; import { showNotification } from './notifications.js'; const serverUrl = 'https://localhost: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
的代码从副作用移到副作用事件中:
¥This is not ideal. You want to re-connect to the chat only if the roomId
has changed. Switching the theme
shouldn’t re-connect to the chat! Move the code reading theme
out of your Effect into an Effect Event:
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://localhost: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'} /> </> ); }
副作用事件中的代码不是反应性的,因此更改 theme
不再使你的副作用重新连接。
¥Code inside Effect Events isn’t reactive, so changing the theme
no longer makes your Effect re-connect.
准备好学习这个主题了吗?
阅读 将事件与副作用分开 以了解如何防止某些值重新触发副作用。
¥Read Separating Events from Effects to learn how to prevent some values from re-triggering Effects.
阅读更多移除副作用依赖
¥Removing Effect dependencies
当你编写副作用时,linter 将验证你是否已将副作用读取的每个反应值(如属性和状态)包含在副作用的依赖列表中。这确保你的副作用与组件的最新属性和状态保持同步。不必要的依赖可能会导致你的副作用运行过于频繁,甚至会造成无限循环。删除它们的方式取决于具体情况。
¥When you write an Effect, the linter will verify that you’ve included every reactive value (like props and state) that the Effect reads in the list of your Effect’s dependencies. This ensures that your Effect remains synchronized with the latest props and state of your component. Unnecessary dependencies may cause your Effect to run too often, or even create an infinite loop. The way you remove them depends on the case.
例如,此副作用依赖于每次编辑输入时都会重新创建的 options
对象:
¥For example, this Effect depends on the options
object which gets re-created every time you edit the input:
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; const serverUrl = 'https://localhost: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
对象的创建移动到副作用中,以便副作用仅依赖于 roomId
字符串:
¥You don’t want the chat to re-connect every time you start typing a message in that chat. To fix this problem, move creation of the options
object inside the Effect so that the Effect only depends on the roomId
string:
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; const serverUrl = 'https://localhost: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
依赖。那是错误的。相反,你更改了周围的代码,使得依赖变得不必要。将依赖列表视为副作用代码使用的所有反应值的列表。你不会有意选择要放在该列表中的内容。该列表描述了你的代码。要更改依赖列表,请更改代码。
¥Notice that you didn’t start by editing the dependency list to remove the options
dependency. That would be wrong. Instead, you changed the surrounding code so that the dependency became unnecessary. Think of the dependency list as a list of all the reactive values used by your Effect’s code. You don’t intentionally choose what to put on that list. The list describes your code. To change the dependency list, change the code.
准备好学习这个主题了吗?
阅读 移除副作用依赖 以了解如何减少副作用重新运行的频率。
¥Read Removing Effect Dependencies to learn how to make your Effect re-run less often.
阅读更多使用自定义钩子重用逻辑
¥Reusing logic with custom Hooks
React 带有内置的钩子,如 useState
、useContext
和 useEffect
。有时,你会希望有一个钩子用于更具体的目的:例如,获取数据、跟踪用户是否在线或连接到聊天室。为此,你可以根据应用的需要创建自己的钩子。
¥React comes with built-in Hooks like useState
, useContext
, and useEffect
. Sometimes, you’ll wish that there was a Hook for some more specific purpose: for example, to fetch data, to keep track of whether the user is online, or to connect to a chat room. To do this, you can create your own Hooks for your application’s needs.
在此示例中,usePointerPosition
自定义钩子跟踪光标位置,而 useDelayedValue
自定义钩子返回一个值,该值是你在特定毫秒数后传递的值。将光标移到沙盒预览区域上以查看跟随光标移动的点轨迹:
¥In this example, the usePointerPosition
custom Hook tracks the cursor position, while useDelayedValue
custom Hook returns a value that’s “lagging behind” the value you passed by a certain number of milliseconds. Move the cursor over the sandbox preview area to see a moving trail of dots following the cursor:
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, }} /> ); }
你可以创建自定义钩子,将它们组合在一起,在它们之间传递数据,并在组件之间重用它们。随着应用的增长,你将减少手动编写的副作用,因为你将能够重用已经编写的自定义钩子。React 社区也维护了很多优秀的自定义钩子。
¥You can create custom Hooks, compose them together, pass data between them, and reuse them between components. As your app grows, you will write fewer Effects by hand because you’ll be able to reuse custom Hooks you already wrote. There are also many excellent custom Hooks maintained by the React community.
准备好学习这个主题了吗?
阅读 使用自定义钩子重用逻辑 以了解如何在组件之间共享逻辑。
¥Read Reusing Logic with Custom Hooks to learn how to share logic between components.
阅读更多下一步是什么?
¥What’s next?
前往 使用引用引用值 开始逐页阅读本章!
¥Head over to Referencing Values with Refs to start reading this chapter page by page!