Một số component của bạn có thể cần kiểm soát và đồng bộ hóa với các hệ thống bên ngoài React. Ví dụ, bạn có thể cần focus một input bằng browser API, phát và tạm dừng một video player được triển khai không sử dụng React, hoặc kết nối và lắng nghe tin nhắn từ một server từ xa. Trong chương này, bạn sẽ học các lối thoát (escape hatches) cho phép bạn “bước ra ngoài” React và kết nối với các hệ thống bên ngoài. Phần lớn logic ứng dụng và data flow của bạn không nên dựa vào những tính năng này.
Trong chương này
- Cách “nhớ” thông tin mà không cần re-render
- Cách truy cập các DOM element được quản lý bởi React
- Cách đồng bộ hóa component với các hệ thống bên ngoài
- Cách loại bỏ những Effect không cần thiết khỏi component của bạn
- Cách lifecycle của Effect khác với lifecycle của một component
- Cách ngăn một số giá trị khỏi việc re-trigger Effect
- Cách làm cho Effect của bạn re-run ít thường xuyên hơn
- Cách chia sẻ logic giữa các component
Nhớ giá trị với refs
Khi bạn muốn một component “nhớ” một số thông tin, nhưng bạn không muốn thông tin đó trigger render mới, bạn có thể sử dụng một ref:
const ref = useRef(0);
Giống như state, refs được React giữ lại giữa các lần re-render. Tuy nhiên, việc thiết lập state sẽ re-render một component. Thay đổi một ref thì không! Bạn có thể truy cập giá trị hiện tại của ref đó thông qua thuộc tính ref.current
.
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> ); }
Một ref giống như một ngăn bí mật của component mà React không theo dõi. Ví dụ, bạn có thể sử dụng refs để lưu trữ timeout IDs, DOM elements, và các object khác không ảnh hưởng đến kết quả render của component.
Ready to learn this topic?
Đọc Nhớ giá trị với Refs để học cách sử dụng refs để nhớ thông tin.
Read MoreThao tác DOM với refs
React tự động cập nhật DOM để phù hợp với kết quả render của bạn, vì vậy component của bạn thường sẽ không cần thao tác với DOM. Tuy nhiên, thỉnh thoảng bạn có thể cần truy cập vào các DOM element được quản lý bởi React—ví dụ, để focus một node, scroll tới nó, hoặc đo kích thước và vị trí của nó. Không có cách built-in nào để làm những việc đó trong React, vì vậy bạn sẽ cần một ref tới DOM node. Ví dụ, nhấp vào button sẽ focus input bằng cách sử dụng một 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> </> ); }
Ready to learn this topic?
Đọc Thao tác DOM với Refs để học cách truy cập các DOM element được quản lý bởi React.
Read MoreĐồng bộ hóa với Effects
Một số component cần đồng bộ hóa với các hệ thống bên ngoài. Ví dụ, bạn có thể muốn kiểm soát một component không phải React dựa trên React state, thiết lập kết nối server, hoặc gửi analytics log khi một component xuất hiện trên màn hình. Không giống như event handlers, cho phép bạn xử lý các sự kiện cụ thể, Effects cho phép bạn chạy một số code sau khi render. Sử dụng chúng để đồng bộ hóa component của bạn với một hệ thống bên ngoài React.
Nhấn Play/Pause một vài lần và xem video player làm thế nào để đồng bộ với giá trị prop 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" /> </> ); }
Nhiều Effect cũng “dọn dẹp” sau chính chúng. Ví dụ, một Effect thiết lập kết nối tới chat server nên trả về một cleanup function để báo cho React biết cách ngắt kết nối component của bạn khỏi 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>; }
Trong môi trường development, React sẽ ngay lập tức chạy và dọn dẹp Effect của bạn thêm một lần nữa. Đó là lý do tại sao bạn thấy "✅ Connecting..."
được in ra hai lần. Điều này đảm bảo rằng bạn không quên triển khai cleanup function.
Ready to learn this topic?
Đọc Đồng bộ hóa với Effects để học cách đồng bộ hóa component với các hệ thống bên ngoài.
Read MoreCó thể bạn không cần một Effect
Effects là một lối thoát khỏi React paradigm. Chúng cho phép bạn “bước ra ngoài” React và đồng bộ hóa component của bạn với một số hệ thống bên ngoài. Nếu không có hệ thống bên ngoài nào liên quan (ví dụ, nếu bạn muốn cập nhật state của một component khi một số props hoặc state thay đổi), bạn không nên cần một Effect. Loại bỏ những Effect không cần thiết sẽ làm cho code của bạn dễ theo dõi hơn, chạy nhanh hơn, và ít dễ gây lỗi hơn.
Có hai trường hợp phổ biến mà bạn không cần Effects:
- Bạn không cần Effects để transform data cho việc render.
- Bạn không cần Effects để xử lý user events.
Ví dụ, bạn không cần một Effect để điều chỉnh một số state dựa trên state khác:
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]);
// ...
}
Thay vào đó, hãy tính toán nhiều nhất có thể trong khi render:
function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// ✅ Good: calculated during rendering
const fullName = firstName + ' ' + lastName;
// ...
}
Tuy nhiên, bạn vẫn cần Effects để đồng bộ hóa với các hệ thống bên ngoài.
Ready to learn this topic?
Đọc Có thể bạn không cần một Effect để học cách loại bỏ những Effect không cần thiết.
Read MoreLifecycle của reactive effects
Effects có một lifecycle khác với component. Component có thể mount, update, hoặc unmount. Một Effect chỉ có thể làm hai việc: bắt đầu đồng bộ hóa cái gì đó, và sau đó ngừng đồng bộ hóa nó. Chu kỳ này có thể xảy ra nhiều lần nếu Effect của bạn phụ thuộc vào props và state thay đổi theo thời gian.
Effect này phụ thuộc vào giá trị của prop roomId
. Props là reactive values, có nghĩa là chúng có thể thay đổi khi re-render. Lưu ý rằng Effect re-synchronizes (và kết nối lại với server) nếu roomId
thay đổi:
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 cung cấp một linter rule để kiểm tra rằng bạn đã chỉ định dependencies của Effect một cách chính xác. Nếu bạn quên chỉ định roomId
trong danh sách dependencies trong ví dụ trên, linter sẽ tự động tìm ra bug đó.
Ready to learn this topic?
Đọc Lifecycle của Reactive Effects để học cách lifecycle của một Effect khác với lifecycle của một component.
Read MoreTách biệt events khỏi Effects
Event handlers chỉ re-run khi bạn thực hiện lại cùng một tương tác. Không giống như event handlers, Effects re-synchronize nếu bất kỳ giá trị nào mà chúng đọc, như props hoặc state, khác so với lần render cuối cùng. Thỉnh thoảng, bạn muốn một sự kết hợp của cả hai hành vi: một Effect re-run để phản hồi với một số giá trị nhưng không phải với những giá trị khác.
Tất cả code bên trong Effects đều reactive. Nó sẽ chạy lại nếu một số reactive value mà nó đọc đã thay đổi do re-render. Ví dụ, Effect này sẽ kết nối lại với chat nếu roomId
hoặc theme
đã thay đổi:
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'} /> </> ); }
Điều này không lý tưởng. Bạn muốn kết nối lại với chat chỉ khi roomId
đã thay đổi. Việc chuyển đổi theme
không nên kết nối lại với chat! Di chuyển code đọc theme
ra khỏi Effect của bạn vào một 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'} /> </> ); }
Code bên trong Effect Events không reactive, vì vậy việc thay đổi theme
không còn làm cho Effect của bạn kết nối lại.
Ready to learn this topic?
Đọc Tách biệt Events khỏi Effects để học cách ngăn một số giá trị khỏi việc re-trigger Effects.
Read MoreLoại bỏ Effect dependencies
Khi bạn viết một Effect, linter sẽ xác minh rằng bạn đã bao gồm mọi reactive value (như props và state) mà Effect đọc trong danh sách dependencies của Effect. Điều này đảm bảo rằng Effect của bạn vẫn đồng bộ với props và state mới nhất của component. Dependencies không cần thiết có thể khiến Effect của bạn chạy quá thường xuyên, hoặc thậm chí tạo ra vòng lặp vô hạn. Cách bạn loại bỏ chúng phụ thuộc vào trường hợp.
Ví dụ, Effect này phụ thuộc vào object options
được tạo lại mỗi khi bạn chỉnh sửa 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} /> </> ); }
Bạn không muốn chat kết nối lại mỗi khi bạn bắt đầu gõ tin nhắn trong chat đó. Để khắc phục vấn đề này, hãy di chuyển việc tạo object options
vào bên trong Effect để Effect chỉ phụ thuộc vào chuỗi 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} /> </> ); }
Lưu ý rằng bạn không bắt đầu bằng việc chỉnh sửa danh sách dependency để loại bỏ dependency options
. Điều đó sẽ là sai. Thay vào đó, bạn đã thay đổi code xung quanh để dependency trở nên không cần thiết. Hãy nghĩ về danh sách dependency như một danh sách tất cả các reactive values được sử dụng bởi code Effect của bạn. Bạn không cố ý chọn những gì để đưa vào danh sách đó. Danh sách mô tả code của bạn. Để thay đổi danh sách dependency, hãy thay đổi code.
Ready to learn this topic?
Đọc Loại bỏ Effect Dependencies để học cách làm cho Effect của bạn re-run ít thường xuyên hơn.
Read MoreTái sử dụng logic với custom Hooks
React đi kèm với các built-in Hooks như useState
, useContext
, và useEffect
. Thỉnh thoảng, bạn sẽ mong muốn có một Hook cho một số mục đích cụ thể hơn: ví dụ, để fetch data, để theo dõi xem user có online không, hoặc để kết nối tới một chat room. Để làm điều này, bạn có thể tạo các Hook của riêng mình cho nhu cầu của ứng dụng.
Trong ví dụ này, custom Hook usePointerPosition
theo dõi vị trí con trỏ chuột, trong khi custom Hook useDelayedValue
trả về một giá trị “chậm trễ” so với giá trị bạn truyền vào một số milliseconds. Di chuyển con trỏ chuột qua vùng preview sandbox để thấy một dãy các chấm di chuyển theo con trỏ chuột:
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, }} /> ); }
Bạn có thể tạo custom Hooks, kết hợp chúng với nhau, truyền data giữa chúng, và tái sử dụng chúng giữa các component. Khi ứng dụng của bạn phát triển, bạn sẽ viết ít Effects bằng tay hơn vì bạn sẽ có thể tái sử dụng custom Hooks mà bạn đã viết. Cũng có nhiều custom Hooks xuất sắc được duy trì bởi cộng đồng React.
Ready to learn this topic?
Đọc Tái sử dụng Logic với Custom Hooks để học cách chia sẻ logic giữa các component.
Read MoreTiếp theo là gì?
Hãy chuyển đến Nhớ giá trị với Refs để bắt đầu đọc chương này từng trang một!