逃生舱口
你的一些组件可能需要控制和同步 React 之外的系统。 例如,您可能需要使用浏览器 API 聚焦输入,播放和暂停未使用 React 实现的视频播放器,或者连接并收听来自远程服务器的消息。 在本章中,您将学习让您“走出去”React 并连接到外部系统的逃生舱口。 大多数应用程序逻辑和数据流不应依赖这些功能。
本章内容预告
- 如何在不重新渲染的情况下“记住”信息
- 如何访问 React 管理的 DOM 元素
- 如何将组件与外部系统同步
- 如何从组件中删除不必要的效果
- Effect 的生命周期与组件的生命周期有何不同
- 如何防止某些值重新触发 Effects
- 如何减少 Effect 重新运行的频率
- 如何在组件之间共享逻辑
使用 ref 引用值
当你想让一个组件“记住”一些信息,但又不想让这些信息触发新的渲染时,你可以使用 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 元素和其他不影响组件渲染输出的对象。
准备好学习这个主题了吗?
阅读使用 ref 引用值了解如何使用 refs 来记住信息。
使用 ref 操作 DOM
React 会自动更新 DOM 以匹配您的渲染输出,因此您的组件不需要经常操作它。 然而,有时您可能需要访问由 React 管理的 DOM 元素——例如,聚焦一个节点,滚动到它,或者测量它的大小和位置。 在 React 中没有内置的方法来做这些事情,所以你需要一个 DOM 节点的引用。 例如,单击按钮将使用 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>
</>
);
}
准备好学习这个主题了吗?
阅读使用 Refs 操作 DOM 以了解如何访问由 React 管理的 DOM 元素。
使用Effect同步
一些组件需要与外部系统同步。 例如,您可能希望根据 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 如何断开你的组件与该服务器的连接:
App.js
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>;
}
chat.js
export function createConnection() {
// A real implementation would actually connect to the server
return {
connect() {
console.log('✅ Connecting...');
},
disconnect() {
console.log('❌ Disconnected.');
}
};
}
在开发中,React 将立即运行并额外清理一次 Effect。 这就是为什么您会看到两次打印“✅ Connecting...”的原因。 这确保您不会忘记实现清理功能。
准备好学习这个主题了吗?
阅读使用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 才能与外部系统同步。
准备好学习这个主题了吗?
阅读您可能不需要效果以了解如何删除不必要的效果。
React Effect的生命周期
Effect 与组件有不同的生命周期。 组件可以挂载、更新或卸载。 Effect 只能做两件事:开始同步某些东西,然后停止同步它。 如果你的 Effect 依赖于随时间变化的props和状态,这个循环可能会发生多次。
此效果取决于 roomId 属性的值。 属性是反应值,这意味着它们可以在重新渲染时改变。 请注意,在您更新 roomId 后,Effect 会重新同步(并重新连接到服务器):
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 规则来检查您是否正确指定了 Effect 的依赖项。 如果您忘记在上述示例的依赖项列表中指定 roomId,linter 会自动找到该错误。
准备好学习这个主题了吗?
阅读反应事件的生命周期,了解 Effect 的生命周期与组件的生命周期有何不同。
将事件与Effect分开
建设中...
本节描述了一个尚未添加到 React 中的实验性 API,因此您还不能使用它。
事件处理程序仅在您再次执行相同的交互时重新运行。 与事件处理程序不同,如果 Effects 读取的某些值(如 prop 或状态变量)与上次渲染时的值不同,则 Effects 会重新同步。 有时,您需要两种行为的混合:一个 Effect 重新运行以响应某些值而不是其他值。
Effects 中的所有代码都是反应式的。 如果它读取的某些反应值由于重新渲染而发生变化,它将再次运行。 例如,如果 roomId 或主题在交互后发生更改,此 Effect 将重新连接到聊天:
App.js
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'}
/>
</>
);
}
chat.js
export function createConnection(serverUrl, roomId) {
// A real implementation would actually connect to the server
let connectedCallback;
let timeout;
return {
connect() {
timeout = setTimeout(() => {
if (connectedCallback) {
connectedCallback();
}
}, 100);
},
on(event, callback) {
if (connectedCallback) {
throw Error('Cannot add the handler twice.');
}
if (event !== 'connected') {
throw Error('Only "connected" event is supported.');
}
connectedCallback = callback;
},
disconnect() {
clearTimeout(timeout);
}
};
}
notifications.js
import Toastify from 'toastify-js';
import 'toastify-js/src/toastify.css';
export function showNotification(message, theme) {
Toastify({
text: message,
duration: 2000,
gravity: 'top',
position: 'right',
style: {
background: theme === 'dark' ? 'black' : 'white',
color: theme === 'dark' ? 'white' : 'black',
},
}).showToast();
}
这并不理想。 仅当 roomId 已更改时,您才想重新连接到聊天。 切换主题不应该重新连接到聊天! 将代码阅读主题从 Effect 移到 Event 函数中:
App.js
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'}
/>
</>
);
}
chat.js
export function createConnection(serverUrl, roomId) {
// A real implementation would actually connect to the server
let connectedCallback;
let timeout;
return {
connect() {
timeout = setTimeout(() => {
if (connectedCallback) {
connectedCallback();
}
}, 100);
},
on(event, callback) {
if (connectedCallback) {
throw Error('Cannot add the handler twice.');
}
if (event !== 'connected') {
throw Error('Only "connected" event is supported.');
}
connectedCallback = callback;
},
disconnect() {
clearTimeout(timeout);
}
};
}
事件函数中的代码不是反应性的,因此更改主题不再使您的效果重新连接。
准备好学习这个主题了吗?
阅读将事件与Effect分开了解如何防止某些值重新触发 Effects。
删除 Effect 依赖项
当您编写 Effect 时,linter 将验证您是否已将 Effect 读取的每个反应值(如 props 和 state)包含在 Effect 的依赖项列表中。 这确保您的 Effect 与组件的最新道具和状态保持同步。 不必要的依赖项可能会导致您的 Effect 运行过于频繁,甚至会造成无限循环。 删除它们的方式取决于具体情况。
例如,此 Effect 取决于每次编辑输入时都会重新创建的选项对象:
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} />
</>
);
}
您不希望每次开始在聊天中输入消息时聊天都重新连接。 要解决此问题,请在 Effect 中移动选项对象的创建,以便 Effect 仅依赖于 roomId 字符串:
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} />
</>
);
}
请注意,您并没有通过编辑依赖项列表来删除选项依赖项。 那是错误的。 相反,您更改了周围的代码,这样依赖性就变得不必要了。 您可以将依赖项列表视为您的 Effect 代码使用的所有反应值的列表。 您不会有意选择要放在该列表中的内容。 该列表描述了您的代码。 要更改依赖项列表,请更改代码。
准备好学习这个主题了吗?
阅读删除 Effect 依赖项了解如何减少 Effect 重新运行的频率。
通过自定义 Hooks 重用逻辑
React 带有内置的 Hook,例如 useState、useContext 和 useEffect。 有时,您会希望有一个 Hook 用于某些更具体的目的:例如,获取数据、跟踪用户是否在线或连接到聊天室。 为此,您可以根据应用程序的需要创建自己的 Hook。
在此示例中,usePointerPosition 自定义 Hook 跟踪光标位置,而 useDelayedValue 自定义 Hook 返回一个“滞后”您传递的值一定毫秒数的值。 将光标移到沙盒预览区域上以查看跟随光标移动的点轨迹:
App.js
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(pos3, 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,
}} />
);
}
usePointerPosition.js
import { useState, useEffect } from 'react';
export function usePointerPosition() {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
function handleMove(e) {
setPosition({ x: e.clientX, y: e.clientY });
}
window.addEventListener('pointermove', handleMove);
return () => window.removeEventListener('pointermove', handleMove);
}, []);
return position;
}
useDelayedValue.js
import { useState, useEffect } from 'react';
export function useDelayedValue(value, delay) {
const [delayedValue, setDelayedValue] = useState(value);
useEffect(() => {
setTimeout(() => {
setDelayedValue(value);
}, delay);
}, [value, delay]);
return delayedValue;
}
您可以创建自定义 Hooks,将它们组合在一起,在它们之间传递数据,并在组件之间重用它们。 随着您的应用程序的增长,您将减少手动编写的 Effects,因为您将能够重用您已经编写的自定义 Hooks。 React 社区也维护了很多优秀的自定义 Hooks。
准备好学习这个主题了吗?
阅读通过自定义 Hooks 重用逻辑了解如何在组件之间共享逻辑。
下一步是什么?
前往使用 Refs 引用值开始逐页阅读本章!
网友评论