逃生艙口
你的一些組件可能需要控制和同步 React 之外的系統(tǒng)。 例如,您可能需要使用瀏覽器 API 聚焦輸入,播放和暫停未使用 React 實(shí)現(xiàn)的視頻播放器,或者連接并收聽(tīng)來(lái)自遠(yuǎn)程服務(wù)器的消息。 在本章中,您將學(xué)習(xí)讓您“走出去”React 并連接到外部系統(tǒng)的逃生艙口。 大多數(shù)應(yīng)用程序邏輯和數(shù)據(jù)流不應(yīng)依賴這些功能。
本章內(nèi)容預(yù)告
- 如何在不重新渲染的情況下“記住”信息
- 如何訪問(wèn) React 管理的 DOM 元素
- 如何將組件與外部系統(tǒng)同步
- 如何從組件中刪除不必要的效果
- Effect 的生命周期與組件的生命周期有何不同
- 如何防止某些值重新觸發(fā) Effects
- 如何減少 Effect 重新運(yùn)行的頻率
- 如何在組件之間共享邏輯
使用 ref 引用值
當(dāng)你想讓一個(gè)組件“記住”一些信息,但又不想讓這些信息觸發(fā)新的渲染時(shí),你可以使用 ref:
const ref = useRef(0);
與狀態(tài)一樣,refs 在重新渲染之間由 React 保留。 但是,設(shè)置狀態(tài)會(huì)重新渲染組件。 更改 ref 不會(huì)! 您可以通過(guò) ref.current 屬性訪問(wèn)該 ref 的當(dāng)前值。
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 來(lái)存儲(chǔ)超時(shí) ID、DOM 元素和其他不影響組件渲染輸出的對(duì)象。
準(zhǔn)備好學(xué)習(xí)這個(gè)主題了嗎?
閱讀使用 ref 引用值了解如何使用 refs 來(lái)記住信息。
使用 ref 操作 DOM
React 會(huì)自動(dòng)更新 DOM 以匹配您的渲染輸出,因此您的組件不需要經(jīng)常操作它。 然而,有時(shí)您可能需要訪問(wèn)由 React 管理的 DOM 元素——例如,聚焦一個(gè)節(jié)點(diǎn),滾動(dòng)到它,或者測(cè)量它的大小和位置。 在 React 中沒(méi)有內(nèi)置的方法來(lái)做這些事情,所以你需要一個(gè) DOM 節(jié)點(diǎn)的引用。 例如,單擊按鈕將使用 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>
</>
);
}
準(zhǔn)備好學(xué)習(xí)這個(gè)主題了嗎?
閱讀使用 Refs 操作 DOM 以了解如何訪問(wèn)由 React 管理的 DOM 元素。
使用Effect同步
一些組件需要與外部系統(tǒng)同步。 例如,您可能希望根據(jù) React 狀態(tài)控制非 React 組件、設(shè)置服務(wù)器連接或在組件出現(xiàn)在屏幕上時(shí)發(fā)送分析日志。 與讓您處理特定事件的事件處理程序不同,Effects 讓您在渲染后運(yùn)行一些代碼。 使用它們將你的組件與 React 之外的一些系統(tǒng)同步。
多次按下播放/暫停鍵,看看視頻播放器如何與 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 建立了一個(gè)到聊天服務(wù)器的連接,它應(yīng)該返回一個(gè)清理函數(shù)來(lái)告訴 React 如何斷開(kāi)你的組件與該服務(wù)器的連接:
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.');
}
};
}
在開(kāi)發(fā)中,React 將立即運(yùn)行并額外清理一次 Effect。 這就是為什么您會(huì)看到兩次打印“? Connecting...”的原因。 這確保您不會(huì)忘記實(shí)現(xiàn)清理功能。
準(zhǔn)備好學(xué)習(xí)這個(gè)主題了嗎?
閱讀使用Effect同步以了解如何將組件與外部系統(tǒng)同步。
你可能不需要Effect
Effects 是 React 范式的逃生通道。 它們讓你“走出”React 并將你的組件與一些外部系統(tǒng)同步。 如果不涉及外部系統(tǒng)(例如,如果您想在某些屬性或狀態(tài)更改時(shí)更新組件的狀態(tài)),則不需要 Effect。 刪除不必要的 Effects 將使您的代碼更易于理解、運(yùn)行速度更快并且更不容易出錯(cuò)。
有兩種常見(jiàn)情況不需要 Effects:
- 您不需要 Effects 來(lái)轉(zhuǎn)換數(shù)據(jù)以進(jìn)行渲染。
- 您不需要 Effects 來(lái)處理用戶事件。
例如,您不需要 Effect 來(lái)根據(jù)其他狀態(tài)調(diào)整某些狀態(tài):
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]);
// ...
}
相反,在渲染時(shí)盡可能多地計(jì)算:
function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// ? Good: calculated during rendering
const fullName = firstName + ' ' + lastName;
// ...
}
但是,您確實(shí)需要 Effects 才能與外部系統(tǒng)同步。
準(zhǔn)備好學(xué)習(xí)這個(gè)主題了嗎?
閱讀您可能不需要效果以了解如何刪除不必要的效果。
React Effect的生命周期
Effect 與組件有不同的生命周期。 組件可以掛載、更新或卸載。 Effect 只能做兩件事:開(kāi)始同步某些東西,然后停止同步它。 如果你的 Effect 依賴于隨時(shí)間變化的props和狀態(tài),這個(gè)循環(huán)可能會(huì)發(fā)生多次。
此效果取決于 roomId 屬性的值。 屬性是反應(yīng)值,這意味著它們可以在重新渲染時(shí)改變。 請(qǐng)注意,在您更新 roomId 后,Effect 會(huì)重新同步(并重新連接到服務(wù)器):
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 提供了一個(gè) linter 規(guī)則來(lái)檢查您是否正確指定了 Effect 的依賴項(xiàng)。 如果您忘記在上述示例的依賴項(xiàng)列表中指定 roomId,linter 會(huì)自動(dòng)找到該錯(cuò)誤。
準(zhǔn)備好學(xué)習(xí)這個(gè)主題了嗎?
閱讀反應(yīng)事件的生命周期,了解 Effect 的生命周期與組件的生命周期有何不同。
將事件與Effect分開(kāi)
建設(shè)中...
本節(jié)描述了一個(gè)尚未添加到 React 中的實(shí)驗(yàn)性 API,因此您還不能使用它。
事件處理程序僅在您再次執(zhí)行相同的交互時(shí)重新運(yùn)行。 與事件處理程序不同,如果 Effects 讀取的某些值(如 prop 或狀態(tài)變量)與上次渲染時(shí)的值不同,則 Effects 會(huì)重新同步。 有時(shí),您需要兩種行為的混合:一個(gè) Effect 重新運(yùn)行以響應(yīng)某些值而不是其他值。
Effects 中的所有代碼都是反應(yīng)式的。 如果它讀取的某些反應(yīng)值由于重新渲染而發(fā)生變化,它將再次運(yùn)行。 例如,如果 roomId 或主題在交互后發(fā)生更改,此 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();
}
這并不理想。 僅當(dāng) roomId 已更改時(shí),您才想重新連接到聊天。 切換主題不應(yīng)該重新連接到聊天! 將代碼閱讀主題從 Effect 移到 Event 函數(shù)中:
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);
}
};
}
事件函數(shù)中的代碼不是反應(yīng)性的,因此更改主題不再使您的效果重新連接。
準(zhǔn)備好學(xué)習(xí)這個(gè)主題了嗎?
閱讀將事件與Effect分開(kāi)了解如何防止某些值重新觸發(fā) Effects。
刪除 Effect 依賴項(xiàng)
當(dāng)您編寫(xiě) Effect 時(shí),linter 將驗(yàn)證您是否已將 Effect 讀取的每個(gè)反應(yīng)值(如 props 和 state)包含在 Effect 的依賴項(xiàng)列表中。 這確保您的 Effect 與組件的最新道具和狀態(tài)保持同步。 不必要的依賴項(xiàng)可能會(huì)導(dǎo)致您的 Effect 運(yùn)行過(guò)于頻繁,甚至?xí)斐蔁o(wú)限循環(huán)。 刪除它們的方式取決于具體情況。
例如,此 Effect 取決于每次編輯輸入時(shí)都會(huì)重新創(chuàng)建的選項(xiàng)對(duì)象:
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} />
</>
);
}
您不希望每次開(kāi)始在聊天中輸入消息時(shí)聊天都重新連接。 要解決此問(wèn)題,請(qǐng)?jiān)?Effect 中移動(dòng)選項(xiàng)對(duì)象的創(chuàng)建,以便 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} />
</>
);
}
請(qǐng)注意,您并沒(méi)有通過(guò)編輯依賴項(xiàng)列表來(lái)刪除選項(xiàng)依賴項(xiàng)。 那是錯(cuò)誤的。 相反,您更改了周?chē)拇a,這樣依賴性就變得不必要了。 您可以將依賴項(xiàng)列表視為您的 Effect 代碼使用的所有反應(yīng)值的列表。 您不會(huì)有意選擇要放在該列表中的內(nèi)容。 該列表描述了您的代碼。 要更改依賴項(xiàng)列表,請(qǐng)更改代碼。
準(zhǔn)備好學(xué)習(xí)這個(gè)主題了嗎?
閱讀刪除 Effect 依賴項(xiàng)了解如何減少 Effect 重新運(yùn)行的頻率。
通過(guò)自定義 Hooks 重用邏輯
React 帶有內(nèi)置的 Hook,例如 useState、useContext 和 useEffect。 有時(shí),您會(huì)希望有一個(gè) Hook 用于某些更具體的目的:例如,獲取數(shù)據(jù)、跟蹤用戶是否在線或連接到聊天室。 為此,您可以根據(jù)應(yīng)用程序的需要?jiǎng)?chuàng)建自己的 Hook。
在此示例中,usePointerPosition 自定義 Hook 跟蹤光標(biāo)位置,而 useDelayedValue 自定義 Hook 返回一個(gè)“滯后”您傳遞的值一定毫秒數(shù)的值。 將光標(biāo)移到沙盒預(yù)覽區(qū)域上以查看跟隨光標(biāo)移動(dòng)的點(diǎn)軌跡:
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;
}
您可以創(chuàng)建自定義 Hooks,將它們組合在一起,在它們之間傳遞數(shù)據(jù),并在組件之間重用它們。 隨著您的應(yīng)用程序的增長(zhǎng),您將減少手動(dòng)編寫(xiě)的 Effects,因?yàn)槟鷮⒛軌蛑赜媚呀?jīng)編寫(xiě)的自定義 Hooks。 React 社區(qū)也維護(hù)了很多優(yōu)秀的自定義 Hooks。
準(zhǔn)備好學(xué)習(xí)這個(gè)主題了嗎?
閱讀通過(guò)自定義 Hooks 重用邏輯了解如何在組件之間共享邏輯。
下一步是什么?
前往使用 Refs 引用值開(kāi)始逐頁(yè)閱讀本章!