useEffect
useEffect
là một React Hook cho phép bạn đồng bộ hóa một component với một hệ thống bên ngoài.
useEffect(setup, dependencies?)
- Tham khảo
- Cách sử dụng
- Kết nối với một hệ thống bên ngoài
- Gói các Effect trong các Hook tùy chỉnh
- Kiểm soát một widget không phải React
- Tìm nạp dữ liệu với Effects
- Chỉ định các dependency reactive
- Cập nhật state dựa trên state trước đó từ một Effect
- Loại bỏ các dependency object không cần thiết
- Loại bỏ các dependency function không cần thiết
- Đọc các props và state mới nhất từ Effect
- Hiển thị nội dung khác nhau trên server và client
- Khắc phục sự cố
- Effect của tôi chạy hai lần khi component được mount
- Effect của tôi chạy sau mỗi lần re-render
- Effect của tôi tiếp tục chạy lại trong một vòng lặp vô hạn
- Logic dọn dẹp của tôi chạy ngay cả khi component của tôi không unmount
- Effect của tôi làm điều gì đó trực quan và tôi thấy một nhấp nháy trước khi nó chạy
Tham khảo
useEffect(setup, dependencies?)
Gọi useEffect
ở cấp cao nhất của component để khai báo một Effect:
import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);
// ...
}
Tham số
-
setup
: Hàm chứa logic Effect của bạn. Hàm thiết lập của bạn cũng có thể trả về một hàm dọn dẹp (cleanup) tùy chọn. Khi component của bạn được thêm vào DOM, React sẽ chạy hàm thiết lập của bạn. Sau mỗi lần re-render với các dependency đã thay đổi, React sẽ chạy hàm dọn dẹp (nếu bạn cung cấp) với các giá trị cũ, và sau đó chạy hàm thiết lập của bạn với các giá trị mới. Sau khi component của bạn bị xóa khỏi DOM, React sẽ chạy hàm dọn dẹp của bạn. -
tùy chọn
dependencies
: Danh sách tất cả các giá trị reactive được tham chiếu bên trong codesetup
. Các giá trị reactive bao gồm props, state và tất cả các biến và hàm được khai báo trực tiếp bên trong phần thân component của bạn. Nếu trình lint của bạn được cấu hình cho React, nó sẽ xác minh rằng mọi giá trị reactive được chỉ định chính xác là một dependency. Danh sách các dependency phải có một số lượng mục không đổi và được viết nội tuyến như[dep1, dep2, dep3]
. React sẽ so sánh từng dependency với giá trị trước đó của nó bằng cách sử dụng so sánhObject.is
. Nếu bạn bỏ qua đối số này, Effect của bạn sẽ chạy lại sau mỗi lần re-render của component. Xem sự khác biệt giữa việc truyền một mảng dependency, một mảng trống và không có dependency nào cả.
Giá trị trả về
useEffect
trả về undefined
.
Lưu ý
-
useEffect
là một Hook, vì vậy bạn chỉ có thể gọi nó ở cấp cao nhất của component hoặc Hook của riêng bạn. Bạn không thể gọi nó bên trong các vòng lặp hoặc điều kiện. Nếu bạn cần điều đó, hãy trích xuất một component mới và di chuyển state vào đó. -
Nếu bạn không cố gắng đồng bộ hóa với một số hệ thống bên ngoài, có lẽ bạn không cần một Effect.
-
Khi Strict Mode được bật, React sẽ chạy thêm một chu kỳ thiết lập + dọn dẹp chỉ dành cho quá trình phát triển trước khi thiết lập thực tế đầu tiên. Đây là một bài kiểm tra áp lực để đảm bảo rằng logic dọn dẹp của bạn “phản ánh” logic thiết lập của bạn và nó dừng hoặc hoàn tác bất cứ điều gì mà thiết lập đang làm. Nếu điều này gây ra sự cố, hãy triển khai hàm dọn dẹp.
-
Nếu một số dependency của bạn là các đối tượng hoặc hàm được xác định bên trong component, có một rủi ro là chúng sẽ khiến Effect chạy lại thường xuyên hơn mức cần thiết. Để khắc phục điều này, hãy loại bỏ các dependency đối tượng và hàm không cần thiết. Bạn cũng có thể trích xuất các bản cập nhật state và logic không reactive ra khỏi Effect của bạn.
-
Nếu Effect của bạn không phải do một tương tác (như một cú nhấp chuột) gây ra, React thường sẽ cho phép trình duyệt vẽ màn hình được cập nhật trước khi chạy Effect của bạn. Nếu Effect của bạn đang làm một cái gì đó trực quan (ví dụ: định vị một tooltip) và độ trễ là đáng chú ý (ví dụ: nó nhấp nháy), hãy thay thế
useEffect
bằnguseLayoutEffect
. -
Nếu Effect của bạn là do một tương tác (như một cú nhấp chuột) gây ra, React có thể chạy Effect của bạn trước khi trình duyệt vẽ màn hình được cập nhật. Điều này đảm bảo rằng kết quả của Effect có thể được quan sát bởi hệ thống sự kiện. Thông thường, điều này hoạt động như mong đợi. Tuy nhiên, nếu bạn phải trì hoãn công việc cho đến sau khi vẽ, chẳng hạn như một
alert()
, bạn có thể sử dụngsetTimeout
. Xem reactwg/react-18/128 để biết thêm thông tin. -
Ngay cả khi Effect của bạn là do một tương tác (như một cú nhấp chuột) gây ra, React có thể cho phép trình duyệt vẽ lại màn hình trước khi xử lý các bản cập nhật state bên trong Effect của bạn. Thông thường, điều này hoạt động như mong đợi. Tuy nhiên, nếu bạn phải chặn trình duyệt vẽ lại màn hình, bạn cần thay thế
useEffect
bằnguseLayoutEffect
. -
Các Effect chỉ chạy trên client. Chúng không chạy trong quá trình server rendering.
Cách sử dụng
Kết nối với một hệ thống bên ngoài
Một số component cần duy trì kết nối với mạng, một số API của trình duyệt hoặc một thư viện của bên thứ ba, trong khi chúng được hiển thị trên trang. Các hệ thống này không được React kiểm soát, vì vậy chúng được gọi là bên ngoài.
Để kết nối component của bạn với một số hệ thống bên ngoài, hãy gọi useEffect
ở cấp cao nhất của component của bạn:
import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);
// ...
}
Bạn cần truyền hai đối số cho useEffect
:
- Một hàm thiết lập với code thiết lập kết nối với hệ thống đó.
- Nó sẽ trả về một hàm dọn dẹp với code dọn dẹp ngắt kết nối khỏi hệ thống đó.
- Một danh sách các dependency bao gồm mọi giá trị từ component của bạn được sử dụng bên trong các hàm đó.
React gọi các hàm thiết lập và dọn dẹp của bạn bất cứ khi nào cần thiết, điều này có thể xảy ra nhiều lần:
- Code thiết lập của bạn chạy khi component của bạn được thêm vào trang (mounts).
- Sau mỗi lần re-render của component của bạn, nơi các dependency đã thay đổi:
- Đầu tiên, code dọn dẹp của bạn chạy với các props và state cũ.
- Sau đó, code thiết lập của bạn chạy với các props và state mới.
- Code dọn dẹp của bạn chạy lần cuối cùng sau khi component của bạn bị xóa khỏi trang (unmounts).
Hãy minh họa chuỗi này cho ví dụ trên.
Khi component ChatRoom
ở trên được thêm vào trang, nó sẽ kết nối với phòng chat với serverUrl
và roomId
ban đầu. Nếu serverUrl
hoặc roomId
thay đổi do kết quả của một re-render (ví dụ: nếu người dùng chọn một phòng chat khác trong một dropdown), Effect của bạn sẽ ngắt kết nối khỏi phòng trước đó và kết nối với phòng tiếp theo. Khi component ChatRoom
bị xóa khỏi trang, Effect của bạn sẽ ngắt kết nối lần cuối cùng.
Để giúp bạn tìm lỗi, trong quá trình phát triển, React chạy thiết lập và dọn dẹp thêm một lần trước thiết lập. Đây là một bài kiểm tra áp lực để xác minh logic Effect của bạn được triển khai chính xác. Nếu điều này gây ra các vấn đề có thể nhìn thấy, thì hàm dọn dẹp của bạn đang thiếu một số logic. Hàm dọn dẹp sẽ dừng hoặc hoàn tác bất cứ điều gì mà hàm thiết lập đã làm. Nguyên tắc chung là người dùng không thể phân biệt giữa việc thiết lập được gọi một lần (như trong production) và một chuỗi thiết lập → dọn dẹp → thiết lập (như trong quá trình phát triển). Xem các giải pháp phổ biến.
Cố gắng viết mọi Effect như một quy trình độc lập và suy nghĩ về một chu kỳ thiết lập/dọn dẹp duy nhất tại một thời điểm. Không quan trọng component của bạn đang mounting, updating hay unmounting. Khi logic dọn dẹp của bạn “phản ánh” chính xác logic thiết lập, Effect của bạn có khả năng phục hồi để chạy thiết lập và dọn dẹp thường xuyên khi cần thiết.
Example 1 of 5: Kết nối với một máy chủ chat
Trong ví dụ này, component ChatRoom
sử dụng một Effect để duy trì kết nối với một hệ thống bên ngoài được xác định trong chat.js
. Nhấn “Open chat” để làm cho component ChatRoom
xuất hiện. Sandbox này chạy ở chế độ phát triển, vì vậy có một chu kỳ kết nối và ngắt kết nối bổ sung, như đã giải thích ở đây. Hãy thử thay đổi roomId
và serverUrl
bằng cách sử dụng dropdown và input, và xem Effect kết nối lại với chat như thế nào. Nhấn “Close chat” để xem Effect ngắt kết nối lần cuối cùng.
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; function ChatRoom({ roomId }) { const [serverUrl, setServerUrl] = useState('https://localhost:1234'); useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.connect(); return () => { connection.disconnect(); }; }, [roomId, serverUrl]); return ( <> <label> Server URL:{' '} <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} /> </label> <h1>Chào mừng đến phòng {roomId}!</h1> </> ); } export default function App() { const [roomId, setRoomId] = useState('general'); const [show, setShow] = useState(false); return ( <> <label> Chọn phòng chat:{' '} <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> <button onClick={() => setShow(!show)}> {show ? 'Đóng chat' : 'Mở chat'} </button> {show && <hr />} {show && <ChatRoom roomId={roomId} />} </> ); }
Gói các Effect trong các Hook tùy chỉnh
Các Effect là một “lối thoát hiểm”: bạn sử dụng chúng khi bạn cần “bước ra ngoài React” và khi không có giải pháp tích hợp tốt hơn cho trường hợp sử dụng của bạn. Nếu bạn thấy mình thường xuyên cần viết Effect theo cách thủ công, thì đó thường là một dấu hiệu cho thấy bạn cần trích xuất một số Hook tùy chỉnh cho các hành vi phổ biến mà các component của bạn dựa vào.
Ví dụ: Hook tùy chỉnh useChatRoom
này “ẩn” logic của Effect của bạn đằng sau một API khai báo hơn:
function useChatRoom({ serverUrl, roomId }) {
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId, serverUrl]);
}
Sau đó, bạn có thể sử dụng nó từ bất kỳ component nào như thế này:
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useChatRoom({
roomId: roomId,
serverUrl: serverUrl
});
// ...
Ngoài ra còn có nhiều Hook tùy chỉnh tuyệt vời cho mọi mục đích có sẵn trong hệ sinh thái React.
Tìm hiểu thêm về việc gói các Effect trong các Hook tùy chỉnh.
Example 1 of 3: Hook useChatRoom
tùy chỉnh
Ví dụ này giống hệt với một trong những ví dụ trước đó, nhưng logic được trích xuất sang một Hook tùy chỉnh.
import { useState } from 'react'; import { useChatRoom } from './useChatRoom.js'; function ChatRoom({ roomId }) { const [serverUrl, setServerUrl] = useState('https://localhost:1234'); useChatRoom({ roomId: roomId, serverUrl: serverUrl }); return ( <> <label> Server URL:{' '} <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} /> </label> <h1>Chào mừng đến phòng {roomId}!</h1> </> ); } export default function App() { const [roomId, setRoomId] = useState('general'); const [show, setShow] = useState(false); return ( <> <label> Chọn phòng chat:{' '} <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> <button onClick={() => setShow(!show)}> {show ? 'Đóng chat' : 'Mở chat'} </button> {show && <hr />} {show && <ChatRoom roomId={roomId} />} </> ); }
Kiểm soát một widget không phải React
Đôi khi, bạn muốn giữ một hệ thống bên ngoài được đồng bộ hóa với một số prop hoặc state của component của bạn.
Ví dụ: nếu bạn có một widget bản đồ của bên thứ ba hoặc một component trình phát video được viết mà không cần React, bạn có thể sử dụng Effect để gọi các phương thức trên nó để làm cho state của nó khớp với state hiện tại của component React của bạn. Effect này tạo một instance của một class MapWidget
được định nghĩa trong map-widget.js
. Khi bạn thay đổi prop zoomLevel
của component Map
, Effect sẽ gọi setZoom()
trên instance class để giữ cho nó được đồng bộ hóa:
import { useRef, useEffect } from 'react'; import { MapWidget } from './map-widget.js'; export default function Map({ zoomLevel }) { const containerRef = useRef(null); const mapRef = useRef(null); useEffect(() => { if (mapRef.current === null) { mapRef.current = new MapWidget(containerRef.current); } const map = mapRef.current; map.setZoom(zoomLevel); }, [zoomLevel]); return ( <div style={{ width: 200, height: 200 }} ref={containerRef} /> ); }
Trong ví dụ này, một hàm dọn dẹp là không cần thiết vì class MapWidget
chỉ quản lý DOM node đã được truyền cho nó. Sau khi component Map
React bị xóa khỏi cây, cả DOM node và instance class MapWidget
sẽ tự động được thu gom rác bởi engine JavaScript của trình duyệt.
Tìm nạp dữ liệu với Effects
Bạn có thể sử dụng Effect để tìm nạp dữ liệu cho component của bạn. Lưu ý rằng nếu bạn sử dụng một framework, việc sử dụng cơ chế tìm nạp dữ liệu tích hợp của framework của bạn sẽ hiệu quả hơn nhiều so với việc viết Effects thủ công.
Nếu bạn muốn tìm nạp dữ liệu từ một Effect thủ công, code của bạn có thể trông như thế này:
import { useState, useEffect } from 'react';
import { fetchBio } from './api.js';
export default function Page() {
const [person, setPerson] = useState('Alice');
const [bio, setBio] = useState(null);
useEffect(() => {
let ignore = false;
setBio(null);
fetchBio(person).then(result => {
if (!ignore) {
setBio(result);
}
});
return () => {
ignore = true;
};
}, [person]);
// ...
Lưu ý biến ignore
được khởi tạo thành false
và được đặt thành true
trong quá trình dọn dẹp. Điều này đảm bảo code của bạn không bị “race conditions”: các phản hồi mạng có thể đến theo một thứ tự khác với thứ tự bạn đã gửi chúng.
import { useState, useEffect } from 'react'; import { fetchBio } from './api.js'; export default function Page() { const [person, setPerson] = useState('Alice'); const [bio, setBio] = useState(null); useEffect(() => { let ignore = false; setBio(null); fetchBio(person).then(result => { if (!ignore) { setBio(result); } }); return () => { ignore = true; } }, [person]); return ( <> <select value={person} onChange={e => { setPerson(e.target.value); }}> <option value="Alice">Alice</option> <option value="Bob">Bob</option> <option value="Taylor">Taylor</option> </select> <hr /> <p><i>{bio ?? 'Loading...'}</i></p> </> ); }
Bạn cũng có thể viết lại bằng cú pháp async
/ await
, nhưng bạn vẫn cần cung cấp một hàm dọn dẹp:
import { useState, useEffect } from 'react'; import { fetchBio } from './api.js'; export default function Page() { const [person, setPerson] = useState('Alice'); const [bio, setBio] = useState(null); useEffect(() => { async function startFetching() { setBio(null); const result = await fetchBio(person); if (!ignore) { setBio(result); } } let ignore = false; startFetching(); return () => { ignore = true; } }, [person]); return ( <> <select value={person} onChange={e => { setPerson(e.target.value); }}> <option value="Alice">Alice</option> <option value="Bob">Bob</option> <option value="Taylor">Taylor</option> </select> <hr /> <p><i>{bio ?? 'Loading...'}</i></p> </> ); }
Việc viết code tìm nạp dữ liệu trực tiếp trong Effects trở nên lặp đi lặp lại và gây khó khăn cho việc thêm các tối ưu hóa như caching và server rendering sau này. Sẽ dễ dàng hơn khi sử dụng một Hook tùy chỉnh—hoặc của riêng bạn hoặc được duy trì bởi cộng đồng.
Tìm hiểu sâu
Việc viết các lệnh gọi fetch
bên trong Effects là một cách phổ biến để tìm nạp dữ liệu, đặc biệt là trong các ứng dụng hoàn toàn phía client. Tuy nhiên, đây là một cách tiếp cận rất thủ công và nó có những nhược điểm đáng kể:
- Effects không chạy trên server. Điều này có nghĩa là HTML được render ban đầu phía server sẽ chỉ bao gồm một state loading mà không có dữ liệu. Máy tính của client sẽ phải tải xuống tất cả JavaScript và render ứng dụng của bạn chỉ để phát hiện ra rằng bây giờ nó cần tải dữ liệu. Điều này không hiệu quả lắm.
- Việc tìm nạp trực tiếp trong Effects giúp dễ dàng tạo ra “network waterfalls”. Bạn render component cha, nó tìm nạp một số dữ liệu, render các component con, và sau đó chúng bắt đầu tìm nạp dữ liệu của chúng. Nếu mạng không nhanh lắm, điều này sẽ chậm hơn đáng kể so với việc tìm nạp tất cả dữ liệu song song.
- Việc tìm nạp trực tiếp trong Effects thường có nghĩa là bạn không preload hoặc cache dữ liệu. Ví dụ: nếu component unmount và sau đó mount lại, nó sẽ phải tìm nạp lại dữ liệu.
- Nó không được tiện dụng lắm. Có khá nhiều code boilerplate liên quan khi viết các lệnh gọi
fetch
theo cách không bị các lỗi như race conditions.
Danh sách các nhược điểm này không dành riêng cho React. Nó áp dụng cho việc tìm nạp dữ liệu trên mount với bất kỳ thư viện nào. Giống như với routing, việc tìm nạp dữ liệu không phải là điều tầm thường để thực hiện tốt, vì vậy chúng tôi khuyên bạn nên sử dụng các cách tiếp cận sau:
- Nếu bạn sử dụng một framework, hãy sử dụng cơ chế tìm nạp dữ liệu tích hợp của nó. Các framework React hiện đại có các cơ chế tìm nạp dữ liệu tích hợp hiệu quả và không mắc phải những cạm bẫy trên.
- Nếu không, hãy cân nhắc sử dụng hoặc xây dựng một cache phía client. Các giải pháp mã nguồn mở phổ biến bao gồm React Query, useSWR, và React Router 6.4+. Bạn cũng có thể xây dựng giải pháp của riêng mình, trong trường hợp đó, bạn sẽ sử dụng Effects bên dưới nhưng cũng thêm logic để loại bỏ các yêu cầu trùng lặp, caching các phản hồi và tránh network waterfalls (bằng cách preload dữ liệu hoặc nâng các yêu cầu dữ liệu lên các route).
Bạn có thể tiếp tục tìm nạp dữ liệu trực tiếp trong Effects nếu không có cách tiếp cận nào trong số này phù hợp với bạn.
Chỉ định các dependency reactive
Lưu ý rằng bạn không thể “chọn” các dependency của Effect của bạn. Mọi giá trị reactive được sử dụng bởi code Effect của bạn phải được khai báo là một dependency. Danh sách dependency của Effect của bạn được xác định bởi code xung quanh:
function ChatRoom({ roomId }) { // Đây là một giá trị reactive
const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // Đây cũng là một giá trị reactive
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Effect này đọc các giá trị reactive này
connection.connect();
return () => connection.disconnect();
}, [serverUrl, roomId]); // ✅ Vì vậy, bạn phải chỉ định chúng làm dependency của Effect của bạn
// ...
}
Nếu serverUrl
hoặc roomId
thay đổi, Effect của bạn sẽ kết nối lại với chat bằng các giá trị mới.
Các giá trị reactive bao gồm các prop và tất cả các biến và hàm được khai báo trực tiếp bên trong component của bạn. Vì roomId
và serverUrl
là các giá trị reactive, bạn không thể xóa chúng khỏi các dependency. Nếu bạn cố gắng bỏ qua chúng và linter của bạn được định cấu hình chính xác cho React, linter sẽ gắn cờ điều này là một lỗi bạn cần sửa:
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, []); // 🔴 React Hook useEffect có các dependency bị thiếu: 'roomId' và 'serverUrl'
// ...
}
Để xóa một dependency, bạn cần phải “chứng minh” cho linter rằng nó không cần phải là một dependency. Ví dụ: bạn có thể di chuyển serverUrl
ra khỏi component của bạn để chứng minh rằng nó không reactive và sẽ không thay đổi khi re-render:
const serverUrl = 'https://localhost:1234'; // Không còn là một giá trị reactive nữa
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ Tất cả các dependency đã được khai báo
// ...
}
Bây giờ serverUrl
không phải là một giá trị reactive (và không thể thay đổi khi re-render), nó không cần phải là một dependency. Nếu code Effect của bạn không sử dụng bất kỳ giá trị reactive nào, danh sách dependency của nó phải trống ([]
):
const serverUrl = 'https://localhost:1234'; // Không còn là một giá trị reactive nữa
const roomId = 'music'; // Không còn là một giá trị reactive nữa
function ChatRoom() {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, []); // ✅ Tất cả các dependency đã được khai báo
// ...
}
Một Effect với các dependency trống không chạy lại khi bất kỳ prop hoặc state nào của component của bạn thay đổi.
Example 1 of 3: Truyền một mảng dependency
Nếu bạn chỉ định các dependency, Effect của bạn sẽ chạy sau lần render ban đầu và sau khi re-render với các dependency đã thay đổi.
useEffect(() => {
// ...
}, [a, b]); // Runs again if a or b are different
Trong ví dụ bên dưới, serverUrl
và roomId
là các giá trị reactive, vì vậy cả hai phải được chỉ định làm dependency. Do đó, việc chọn một phòng khác trong dropdown hoặc chỉnh sửa input URL của server sẽ khiến chat kết nối lại. Tuy nhiên, vì message
không được sử dụng trong Effect (và do đó nó không phải là một dependency), việc chỉnh sửa message sẽ không kết nối lại với chat.
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; function ChatRoom({ roomId }) { const [serverUrl, setServerUrl] = useState('https://localhost:1234'); const [message, setMessage] = useState(''); useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.connect(); return () => { connection.disconnect(); }; }, [serverUrl, roomId]); return ( <> <label> Server URL:{' '} <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} /> </label> <h1>Chào mừng đến phòng {roomId}!</h1> <label> Tin nhắn của bạn:{' '} <input value={message} onChange={e => setMessage(e.target.value)} /> </label> </> ); } export default function App() { const [show, setShow] = useState(false); const [roomId, setRoomId] = useState('general'); return ( <> <label> Chọn phòng chat:{' '} <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> <button onClick={() => setShow(!show)}> {show ? 'Đóng chat' : 'Mở chat'} </button> </label> {show && <hr />} {show && <ChatRoom roomId={roomId}/>} </> ); }
Cập nhật state dựa trên state trước đó từ một Effect
Khi bạn muốn cập nhật state dựa trên state trước đó từ một Effect, bạn có thể gặp phải một vấn đề:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setCount(count + 1); // Bạn muốn tăng bộ đếm mỗi giây...
}, 1000)
return () => clearInterval(intervalId);
}, [count]); // 🚩 ... nhưng việc chỉ định `count` làm dependency luôn reset interval.
// ...
}
Vì count
là một giá trị reactive, nó phải được chỉ định trong danh sách các dependency. Tuy nhiên, điều đó khiến Effect dọn dẹp và thiết lập lại mỗi khi count
thay đổi. Điều này không lý tưởng.
Để khắc phục điều này, truyền hàm cập nhật state c => c + 1
cho setCount
:
import { useState, useEffect } from 'react'; export default function Counter() { const [count, setCount] = useState(0); useEffect(() => { const intervalId = setInterval(() => { setCount(c => c + 1); // ✅ Truyền một hàm cập nhật state }, 1000); return () => clearInterval(intervalId); }, []); // ✅ Bây giờ count không phải là một dependency return <h1>{count}</h1>; }
Bây giờ bạn đang truyền c => c + 1
thay vì count + 1
, Effect của bạn không còn cần phải phụ thuộc vào count
. Do kết quả của việc sửa lỗi này, nó sẽ không cần phải dọn dẹp và thiết lập lại interval mỗi khi count
thay đổi.
Loại bỏ các dependency object không cần thiết
Nếu Effect của bạn phụ thuộc vào một đối tượng hoặc một hàm được tạo trong quá trình rendering, nó có thể chạy quá thường xuyên. Ví dụ: Effect này kết nối lại sau mỗi lần render vì đối tượng options
khác nhau cho mỗi lần render:
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
const options = { // 🚩 Đối tượng này được tạo mới hoàn toàn sau mỗi lần re-render
serverUrl: serverUrl,
roomId: roomId
};
useEffect(() => {
const connection = createConnection(options); // Nó được sử dụng bên trong Effect
connection.connect();
return () => connection.disconnect();
}, [options]); // 🚩 Kết quả là, các dependency này luôn khác nhau sau mỗi lần re-render
// ...
Tránh sử dụng một đối tượng được tạo trong quá trình rendering làm dependency. Thay vào đó, hãy tạo đối tượng bên trong Effect:
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>Chào mừng đến phòng {roomId}!</h1> <input value={message} onChange={e => setMessage(e.target.value)} /> </> ); } export default function App() { const [roomId, setRoomId] = useState('general'); return ( <> <label> Chọn phòng chat:{' '} <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ây giờ bạn tạo đối tượng options
bên trong Effect, bản thân Effect chỉ phụ thuộc vào chuỗi roomId
.
Với sửa đổi này, việc nhập vào input sẽ không kết nối lại chat. Không giống như một đối tượng được tạo lại, một chuỗi như roomId
không thay đổi trừ khi bạn gán nó một giá trị khác. Đọc thêm về loại bỏ dependency.
Loại bỏ các dependency function không cần thiết
Nếu Effect của bạn phụ thuộc vào một đối tượng hoặc một hàm được tạo trong quá trình render, nó có thể chạy quá thường xuyên. Ví dụ: Effect này kết nối lại sau mỗi lần render vì hàm createOptions
khác nhau sau mỗi lần render:
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
function createOptions() { // 🚩 Hàm này được tạo mới hoàn toàn sau mỗi lần re-render
return {
serverUrl: serverUrl,
roomId: roomId
};
}
useEffect(() => {
const options = createOptions(); // Nó được sử dụng bên trong Effect
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // 🚩 Kết quả là, các dependency này luôn khác nhau sau mỗi lần re-render
// ...
Bản thân việc tạo một hàm mới hoàn toàn sau mỗi lần re-render không phải là một vấn đề. Bạn không cần phải tối ưu hóa điều đó. Tuy nhiên, nếu bạn sử dụng nó như một dependency của Effect, nó sẽ khiến Effect của bạn chạy lại sau mỗi lần re-render.
Tránh sử dụng một hàm được tạo trong quá trình render làm dependency. Thay vào đó, hãy khai báo nó bên trong Effect:
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; const serverUrl = 'https://localhost:1234'; function ChatRoom({ roomId }) { const [message, setMessage] = useState(''); useEffect(() => { function createOptions() { return { serverUrl: serverUrl, roomId: roomId }; } const options = createOptions(); const connection = createConnection(options); connection.connect(); return () => connection.disconnect(); }, [roomId]); return ( <> <h1>Chào mừng đến phòng {roomId}!</h1> <input value={message} onChange={e => setMessage(e.target.value)} /> </> ); } export default function App() { const [roomId, setRoomId] = useState('general'); return ( <> <label> Chọn phòng chat:{' '} <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ây giờ bạn đã định nghĩa hàm createOptions
bên trong Effect, bản thân Effect chỉ phụ thuộc vào chuỗi roomId
. Với sửa đổi này, việc nhập vào input sẽ không kết nối lại chat. Không giống như một hàm được tạo lại, một chuỗi như roomId
không thay đổi trừ khi bạn gán nó một giá trị khác. Đọc thêm về loại bỏ dependency.
Đọc các props và state mới nhất từ Effect
Theo mặc định, khi bạn đọc một giá trị reactive từ Effect, bạn phải thêm nó làm dependency. Điều này đảm bảo rằng Effect của bạn “phản ứng” với mọi thay đổi của giá trị đó. Đối với hầu hết các dependency, đó là hành vi bạn muốn.
Tuy nhiên, đôi khi bạn sẽ muốn đọc các props và state mới nhất từ Effect mà không cần “phản ứng” với chúng. Ví dụ: hãy tưởng tượng bạn muốn ghi lại số lượng các mặt hàng trong giỏ hàng cho mỗi lần truy cập trang:
function Page({ url, shoppingCart }) {
useEffect(() => {
logVisit(url, shoppingCart.length);
}, [url, shoppingCart]); // ✅ Tất cả các dependency đã được khai báo
// ...
}
Điều gì sẽ xảy ra nếu bạn muốn ghi lại một lượt truy cập trang mới sau mỗi thay đổi url
, nhưng không phải nếu chỉ shoppingCart
thay đổi? Bạn không thể loại trừ shoppingCart
khỏi các dependency mà không phá vỡ các quy tắc reactive. Tuy nhiên, bạn có thể thể hiện rằng bạn không muốn một đoạn code “phản ứng” với các thay đổi mặc dù nó được gọi từ bên trong Effect. Khai báo một Effect Event với Hook useEffectEvent
và di chuyển code đọc shoppingCart
vào bên trong nó:
function Page({ url, shoppingCart }) {
const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, shoppingCart.length)
});
useEffect(() => {
onVisit(url);
}, [url]); // ✅ Tất cả các dependency đã được khai báo
// ...
}
Effect Events không reactive và phải luôn được bỏ qua khỏi các dependency của Effect của bạn. Đây là điều cho phép bạn đặt code không reactive (nơi bạn có thể đọc giá trị mới nhất của một số props và state) bên trong chúng. Bằng cách đọc shoppingCart
bên trong onVisit
, bạn đảm bảo rằng shoppingCart
sẽ không chạy lại Effect của bạn.
Đọc thêm về cách Effect Events cho phép bạn tách code reactive và không reactive.
Hiển thị nội dung khác nhau trên server và client
Nếu ứng dụng của bạn sử dụng server rendering (hoặc trực tiếp hoặc thông qua một framework), component của bạn sẽ render trong hai môi trường khác nhau. Trên server, nó sẽ render để tạo ra HTML ban đầu. Trên client, React sẽ chạy lại code rendering để nó có thể đính kèm các trình xử lý sự kiện của bạn vào HTML đó. Đây là lý do tại sao, để hydration hoạt động, đầu ra render ban đầu của bạn phải giống hệt nhau trên client và server.
Trong một số trường hợp hiếm hoi, bạn có thể cần hiển thị nội dung khác nhau trên client. Ví dụ: nếu ứng dụng của bạn đọc một số dữ liệu từ localStorage
, thì nó không thể thực hiện điều đó trên server. Đây là cách bạn có thể triển khai điều này:
function MyComponent() {
const [didMount, setDidMount] = useState(false);
useEffect(() => {
setDidMount(true);
}, []);
if (didMount) {
// ... trả về JSX chỉ dành cho client ...
} else {
// ... trả về JSX ban đầu ...
}
}
Trong khi ứng dụng đang tải, người dùng sẽ thấy đầu ra render ban đầu. Sau đó, khi nó được tải và hydrate, Effect của bạn sẽ chạy và đặt didMount
thành true
, kích hoạt re-render. Điều này sẽ chuyển sang đầu ra render chỉ dành cho client. Các Effect không chạy trên server, vì vậy đây là lý do tại sao didMount
là false
trong quá trình server render ban đầu.
Sử dụng pattern này một cách tiết kiệm. Hãy nhớ rằng người dùng có kết nối chậm sẽ thấy nội dung ban đầu trong một khoảng thời gian khá dài - có khả năng là nhiều giây - vì vậy bạn không muốn thực hiện các thay đổi khó chịu đối với giao diện của component. Trong nhiều trường hợp, bạn có thể tránh sự cần thiết của điều này bằng cách hiển thị có điều kiện những thứ khác nhau bằng CSS.
Khắc phục sự cố
Effect của tôi chạy hai lần khi component được mount
Khi Strict Mode được bật, trong quá trình phát triển, React sẽ chạy thiết lập và dọn dẹp thêm một lần trước khi thiết lập thực tế.
Đây là một bài kiểm tra áp lực để xác minh logic Effect của bạn được triển khai chính xác. Nếu điều này gây ra các vấn đề có thể nhìn thấy, thì hàm dọn dẹp của bạn đang thiếu một số logic. Hàm dọn dẹp sẽ dừng hoặc hoàn tác bất cứ điều gì mà hàm thiết lập đã làm. Nguyên tắc chung là người dùng không thể phân biệt giữa việc thiết lập được gọi một lần (như trong production) và một chuỗi thiết lập → dọn dẹp → thiết lập (như trong quá trình phát triển).
Đọc thêm về cách điều này giúp tìm lỗi và cách sửa logic của bạn.
Effect của tôi chạy sau mỗi lần re-render
Đầu tiên, hãy kiểm tra xem bạn có quên chỉ định mảng dependency hay không:
useEffect(() => {
// ...
}); // 🚩 Không có mảng dependency: chạy lại sau mỗi lần render!
Nếu bạn đã chỉ định mảng dependency nhưng Effect của bạn vẫn chạy lại trong một vòng lặp, thì đó là vì một trong các dependency của bạn khác nhau sau mỗi lần re-render.
Bạn có thể gỡ lỗi vấn đề này bằng cách ghi thủ công các dependency của bạn vào console:
useEffect(() => {
// ..
}, [serverUrl, roomId]);
console.log([serverUrl, roomId]);
Sau đó, bạn có thể nhấp chuột phải vào các mảng từ các re-render khác nhau trong console và chọn “Store as a global variable” cho cả hai. Giả sử cái đầu tiên được lưu dưới dạng temp1
và cái thứ hai được lưu dưới dạng temp2
, sau đó bạn có thể sử dụng console của trình duyệt để kiểm tra xem mỗi dependency trong cả hai mảng có giống nhau hay không:
Object.is(temp1[0], temp2[0]); // Dependency đầu tiên có giống nhau giữa các mảng không?
Object.is(temp1[1], temp2[1]); // Dependency thứ hai có giống nhau giữa các mảng không?
Object.is(temp1[2], temp2[2]); // ... và cứ thế cho mọi dependency ...
Khi bạn tìm thấy dependency khác nhau sau mỗi lần re-render, bạn thường có thể sửa nó theo một trong những cách sau:
- Cập nhật state dựa trên state trước đó từ Effect
- Loại bỏ các dependency object không cần thiết
- Loại bỏ các dependency function không cần thiết
- Đọc các props và state mới nhất từ Effect
Phương sách cuối cùng (nếu các phương pháp này không giúp ích), hãy bọc việc tạo nó bằng useMemo
hoặc useCallback
(cho các function).
Effect của tôi tiếp tục chạy lại trong một vòng lặp vô hạn
Nếu Effect của bạn chạy trong một vòng lặp vô hạn, thì hai điều này phải đúng:
- Effect của bạn đang cập nhật một số state.
- State đó dẫn đến re-render, khiến các dependency của Effect thay đổi.
Trước khi bạn bắt đầu sửa vấn đề, hãy tự hỏi Effect của bạn có kết nối với một số hệ thống bên ngoài (như DOM, mạng, một widget của bên thứ ba, v.v.) hay không. Tại sao Effect của bạn cần đặt state? Nó có đồng bộ hóa với hệ thống bên ngoài đó không? Hay bạn đang cố gắng quản lý luồng dữ liệu của ứng dụng của mình bằng nó?
Nếu không có hệ thống bên ngoài, hãy xem xét liệu loại bỏ Effect hoàn toàn có đơn giản hóa logic của bạn hay không.
Nếu bạn thực sự đang đồng bộ hóa với một số hệ thống bên ngoài, hãy suy nghĩ về lý do và trong điều kiện nào Effect của bạn sẽ cập nhật state. Có điều gì đó đã thay đổi ảnh hưởng đến đầu ra hình ảnh của component của bạn không? Nếu bạn cần theo dõi một số dữ liệu không được sử dụng bởi rendering, thì ref (không kích hoạt re-render) có thể phù hợp hơn. Xác minh Effect của bạn không cập nhật state (và kích hoạt re-render) nhiều hơn mức cần thiết.
Cuối cùng, nếu Effect của bạn đang cập nhật state vào đúng thời điểm, nhưng vẫn còn một vòng lặp, thì đó là vì bản cập nhật state đó dẫn đến một trong các dependency của Effect thay đổi. Đọc cách gỡ lỗi các thay đổi dependency.
Logic dọn dẹp của tôi chạy ngay cả khi component của tôi không unmount
Hàm dọn dẹp chạy không chỉ trong quá trình unmount, mà trước mỗi lần re-render với các dependency đã thay đổi. Ngoài ra, trong quá trình phát triển, React chạy thiết lập + dọn dẹp thêm một lần ngay sau khi component được mount.
Nếu bạn có code dọn dẹp mà không có code thiết lập tương ứng, thì đó thường là một dấu hiệu xấu:
useEffect(() => {
// 🔴 Tránh: Logic dọn dẹp mà không có logic thiết lập tương ứng
return () => {
doSomething();
};
}, []);
Logic dọn dẹp của bạn phải “đối xứng” với logic thiết lập và phải dừng hoặc hoàn tác bất cứ điều gì mà thiết lập đã làm:
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);
Tìm hiểu cách vòng đời Effect khác với vòng đời của component.
Effect của tôi làm điều gì đó trực quan và tôi thấy một nhấp nháy trước khi nó chạy
Nếu Effect của bạn phải chặn trình duyệt vẽ màn hình, hãy thay thế useEffect
bằng useLayoutEffect
. Lưu ý rằng điều này không cần thiết cho phần lớn các Effect. Bạn sẽ chỉ cần điều này nếu điều quan trọng là phải chạy Effect của bạn trước khi trình duyệt vẽ: ví dụ: để đo và định vị một tooltip trước khi người dùng nhìn thấy nó.