Loại bỏ các dependency không cần thiết của Effect
Khi bạn viết một Effect, linter sẽ xác minh rằng bạn đã bao gồm mọi giá trị reactive (như props và state) mà Effect đọc trong danh sách các dependency của Effect đó. Điều này đảm bảo rằng Effect của bạn luôn đồng bộ với props và state mới nhất của component. Các dependency không cần thiết có thể khiến Effect chạy quá thường xuyên, hoặc thậm chí tạo ra một vòng lặp vô hạn. Hãy làm theo hướng dẫn này để xem xét và loại bỏ các dependency không cần thiết khỏi Effect của bạn.
Bạn sẽ được học
- Cách sửa vòng lặp dependency Effect vô hạn
- Phải làm gì khi bạn muốn loại bỏ một dependency
- Cách đọc một giá trị từ Effect mà không “phản ứng” với nó
- Cách và tại sao nên tránh các dependency là object và function
- Tại sao việc bỏ qua dependency linter là nguy hiểm, và phải làm gì thay thế
Các dependency nên khớp với code
Khi bạn viết một Effect, trước tiên bạn chỉ định cách bắt đầu và dừng những gì bạn muốn Effect thực hiện:
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
// ...
}
Sau đó, nếu bạn để các dependency của Effect trống ([]
), linter sẽ gợi ý các dependency đúng:
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(); }, []); // <-- Fix the mistake here! 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} /> </> ); }
Điền chúng theo những gì linter nói:
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ All dependencies declared
// ...
}
Effect “phản ứng” với các giá trị reactive. Vì roomId
là một giá trị reactive (nó có thể thay đổi do một lần render lại), linter xác minh rằng bạn đã chỉ định nó như một dependency. Nếu roomId
nhận một giá trị khác, React sẽ đồng bộ lại Effect của bạn. Điều này đảm bảo rằng cuộc trò chuyện luôn kết nối với phòng được chọn và “phản ứng” với dropdown:
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} /> </> ); }
Để loại bỏ một dependency, hãy chứng minh rằng nó không phải là dependency
Lưu ý rằng bạn không thể “chọn” các dependency của Effect. Mọi giá trị reactive được sử dụng bởi code Effect của bạn đều phải được khai báo trong danh sách dependency của bạn. Danh sách dependency được xác định bởi code xung quanh:
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) { // This is a reactive value
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // This Effect reads that reactive value
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ So you must specify that reactive value as a dependency of your Effect
// ...
}
Giá trị reactive bao gồm props và tất cả các biến và function được khai báo trực tiếp bên trong component của bạn. Vì roomId
là một giá trị reactive, bạn không thể loại bỏ nó khỏi danh sách dependency. Linter sẽ không cho phép:
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, []); // 🔴 React Hook useEffect has a missing dependency: 'roomId'
// ...
}
Và linter sẽ đúng! Vì roomId
có thể thay đổi theo thời gian, điều này sẽ tạo ra một bug trong code của bạn.
Để loại bỏ một dependency, hãy “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 roomId
ra khỏi component để chứng minh rằng nó không reactive và sẽ không thay đổi khi render lại:
const serverUrl = 'https://localhost:1234';
const roomId = 'music'; // Not a reactive value anymore
function ChatRoom() {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, []); // ✅ All dependencies declared
// ...
}
Bây giờ roomId
không phải là một giá trị reactive (và không thể thay đổi khi render lại), nó không cần phải là một dependency:
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; const serverUrl = 'https://localhost:1234'; const roomId = 'music'; export default function ChatRoom() { useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.connect(); return () => connection.disconnect(); }, []); return <h1>Welcome to the {roomId} room!</h1>; }
Đây là lý do tại sao bây giờ bạn có thể chỉ định danh sách dependency trống ([]
). Effect của bạn thực sự không phụ thuộc vào bất kỳ giá trị reactive nào nữa, vì vậy nó thực sự không cần chạy lại khi bất kỳ props hoặc state nào của component thay đổi.
Để thay đổi các dependency, hãy thay đổi code
Bạn có thể nhận thấy một mô hình trong quy trình làm việc của mình:
- Trước tiên, bạn thay đổi code của Effect hoặc cách các giá trị reactive được khai báo.
- Sau đó, bạn làm theo linter và điều chỉnh các dependency để khớp với code bạn đã thay đổi.
- Nếu bạn không hài lòng với danh sách dependency, bạn quay lại bước đầu tiên (và thay đổi code lại).
Phần cuối cùng rất quan trọng. Nếu bạn muốn thay đổi các dependency, hãy thay đổi code xung quanh trước. Bạn có thể nghĩ về danh sách dependency như một danh sách tất cả các giá trị reactive được sử dụng bởi code Effect của bạn. Bạn không chọn cái 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.
Điều này có thể giống như việc giải một phương trình. Bạn có thể bắt đầu với một mục tiêu (ví dụ, để loại bỏ một dependency), và bạn cần “tìm” code phù hợp với mục tiêu đó. Không phải ai cũng thấy việc giải phương trình thú vị, và điều tương tự có thể nói về việc viết Effect! May mắn thay, có một danh sách các công thức phổ biến mà bạn có thể thử bên dưới.
Tìm hiểu sâu
Việc bỏ qua linter dẫn đến những bug rất khó hiểu và khó tìm và sửa. Đây là một ví dụ:
import { useState, useEffect } from 'react'; export default function Timer() { const [count, setCount] = useState(0); const [increment, setIncrement] = useState(1); function onTick() { setCount(count + increment); } useEffect(() => { const id = setInterval(onTick, 1000); return () => clearInterval(id); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return ( <> <h1> Counter: {count} <button onClick={() => setCount(0)}>Reset</button> </h1> <hr /> <p> Every second, increment by: <button disabled={increment === 0} onClick={() => { setIncrement(i => i - 1); }}>–</button> <b>{increment}</b> <button onClick={() => { setIncrement(i => i + 1); }}>+</button> </p> </> ); }
Giả sử bạn muốn chạy Effect “chỉ khi mount”. Bạn đã đọc rằng dependency trống ([]
) làm điều đó, vì vậy bạn quyết định bỏ qua linter, và cưỡng chế chỉ định []
làm dependency.
Bộ đếm này được cho là sẽ tăng mỗi giây theo số lượng có thể cấu hình bằng hai nút. Tuy nhiên, vì bạn đã “nói dối” React rằng Effect này không phụ thuộc vào gì, React mãi mãi tiếp tục sử dụng function onTick
từ lần render ban đầu. Trong lần render đó, count
là 0
và increment
là 1
. Đây là lý do tại sao onTick
từ lần render đó luôn gọi setCount(0 + 1)
mỗi giây, và bạn luôn thấy 1
. Những bug như thế này khó sửa hơn khi chúng lan rộng qua nhiều component.
Luôn có một giải pháp tốt hơn việc bỏ qua linter! Để sửa code này, bạn cần thêm onTick
vào danh sách dependency. (Để đảm bảo interval chỉ được thiết lập một lần, làm onTick
thành một Effect Event.)
Chúng tôi khuyến nghị coi lỗi lint dependency như một lỗi biên dịch. Nếu bạn không bỏ qua nó, bạn sẽ không bao giờ thấy những bug như thế này. Phần còn lại của trang này tài liệu hóa các giải pháp thay thế cho trường hợp này và các trường hợp khác.
Loại bỏ các dependency không cần thiết
Mỗi khi bạn điều chỉnh các dependency của Effect để phản ánh code, hãy nhìn vào danh sách dependency. Có hợp lý không khi Effect chạy lại khi bất kỳ dependency nào trong số này thay đổi? Đôi khi, câu trả lời là “không”:
- Bạn có thể muốn thực thi lại các phần khác nhau của Effect trong những điều kiện khác nhau.
- Bạn có thể chỉ muốn đọc giá trị mới nhất của một số dependency thay vì “phản ứng” với những thay đổi của nó.
- Một dependency có thể thay đổi quá thường xuyên một cách không cố ý vì nó là một object hoặc function.
Để tìm giải pháp đúng, bạn sẽ cần trả lời một vài câu hỏi về Effect của mình. Hãy cùng xem qua chúng.
Code này có nên chuyển sang event handler không?
Điều đầu tiên bạn nên nghĩ đến là liệu code này có nên là một Effect hay không.
Hãy tưởng tượng một form. Khi submit, bạn đặt biến state submitted
thành true
. Bạn cần gửi một POST request và hiển thị một thông báo. Bạn đã đặt logic này bên trong một Effect “phản ứng” với submitted
là true
:
function Form() {
const [submitted, setSubmitted] = useState(false);
useEffect(() => {
if (submitted) {
// 🔴 Avoid: Event-specific logic inside an Effect
post('/api/register');
showNotification('Successfully registered!');
}
}, [submitted]);
function handleSubmit() {
setSubmitted(true);
}
// ...
}
Sau đó, bạn muốn tạo kiểu cho thông báo theo theme hiện tại, vì vậy bạn đọc theme hiện tại. Vì theme
được khai báo trong thân component, nó là một giá trị reactive, vì vậy bạn thêm nó như một dependency:
function Form() {
const [submitted, setSubmitted] = useState(false);
const theme = useContext(ThemeContext);
useEffect(() => {
if (submitted) {
// 🔴 Avoid: Event-specific logic inside an Effect
post('/api/register');
showNotification('Successfully registered!', theme);
}
}, [submitted, theme]); // ✅ All dependencies declared
function handleSubmit() {
setSubmitted(true);
}
// ...
}
Bằng cách làm điều này, bạn đã tạo ra một bug. Hãy tưởng tượng bạn submit form trước, sau đó chuyển đổi giữa theme Dark và Light. theme
sẽ thay đổi, Effect sẽ chạy lại, và vì vậy nó sẽ hiển thị cùng một thông báo lần nữa!
Vấn đề ở đây là điều này không nên là một Effect ngay từ đầu. Bạn muốn gửi POST request này và hiển thị thông báo để phản hồi việc submit form, đó là một tương tác cụ thể. Để chạy một số code phản hồi tương tác cụ thể, hãy đặt logic đó trực tiếp vào event handler tương ứng:
function Form() {
const theme = useContext(ThemeContext);
function handleSubmit() {
// ✅ Good: Event-specific logic is called from event handlers
post('/api/register');
showNotification('Successfully registered!', theme);
}
// ...
}
Bây giờ code ở trong event handler, nó không phải là reactive—vì vậy nó sẽ chỉ chạy khi người dùng submit form. Đọc thêm về lựa chọn giữa event handler và Effect và cách xóa các Effect không cần thiết.
Effect của bạn có đang làm nhiều việc không liên quan không?
Câu hỏi tiếp theo bạn nên tự hỏi là liệu Effect của bạn có đang làm nhiều việc không liên quan.
Hãy tưởng tượng bạn đang tạo một form vận chuyển nơi người dùng cần chọn thành phố và khu vực của họ. Bạn fetch danh sách cities
từ server theo country
được chọn để hiển thị chúng trong dropdown:
function ShippingForm({ country }) {
const [cities, setCities] = useState(null);
const [city, setCity] = useState(null);
useEffect(() => {
let ignore = false;
fetch(`/api/cities?country=${country}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setCities(json);
}
});
return () => {
ignore = true;
};
}, [country]); // ✅ All dependencies declared
// ...
Đây là một ví dụ tốt về fetch data trong Effect. Bạn đang đồng bộ state cities
với mạng theo prop country
. Bạn không thể làm điều này trong event handler vì bạn cần fetch ngay khi ShippingForm
được hiển thị và bất cứ khi nào country
thay đổi (bất kể tương tác nào gây ra).
Bây giờ, giả sử bạn đang thêm một select box thứ hai cho các khu vực thành phố, sẽ fetch areas
cho city
hiện tại được chọn. Bạn có thể bắt đầu bằng cách thêm một cuộc gọi fetch
thứ hai cho danh sách các khu vực bên trong cùng một Effect:
function ShippingForm({ country }) {
const [cities, setCities] = useState(null);
const [city, setCity] = useState(null);
const [areas, setAreas] = useState(null);
useEffect(() => {
let ignore = false;
fetch(`/api/cities?country=${country}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setCities(json);
}
});
// 🔴 Avoid: A single Effect synchronizes two independent processes
if (city) {
fetch(`/api/areas?city=${city}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setAreas(json);
}
});
}
return () => {
ignore = true;
};
}, [country, city]); // ✅ All dependencies declared
// ...
Tuy nhiên, vì Effect bây giờ sử dụng biến state city
, bạn đã phải thêm city
vào danh sách dependency. Điều đó, lần lượt, gây ra một vấn đề: khi người dùng chọn một thành phố khác, Effect sẽ chạy lại và gọi fetchCities(country)
. Kết quả là, bạn sẽ fetch lại danh sách các thành phố một cách không cần thiết nhiều lần.
Vấn đề với code này là bạn đang đồng bộ hai thứ khác nhau không liên quan:
- Bạn muốn đồng bộ state
cities
với mạng dựa trên propcountry
. - Bạn muốn đồng bộ state
areas
với mạng dựa trên statecity
.
Chia logic thành hai Effect, mỗi Effect phản ứng với prop mà nó cần đồng bộ:
function ShippingForm({ country }) {
const [cities, setCities] = useState(null);
useEffect(() => {
let ignore = false;
fetch(`/api/cities?country=${country}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setCities(json);
}
});
return () => {
ignore = true;
};
}, [country]); // ✅ All dependencies declared
const [city, setCity] = useState(null);
const [areas, setAreas] = useState(null);
useEffect(() => {
if (city) {
let ignore = false;
fetch(`/api/areas?city=${city}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setAreas(json);
}
});
return () => {
ignore = true;
};
}
}, [city]); // ✅ All dependencies declared
// ...
Bây giờ Effect đầu tiên chỉ chạy lại nếu country
thay đổi, trong khi Effect thứ hai chạy lại khi city
thay đổi. Bạn đã tách chúng theo mục đích: hai thứ khác nhau được đồng bộ bởi hai Effect riêng biệt. Hai Effect riêng biệt có hai danh sách dependency riêng biệt, vì vậy chúng sẽ không kích hoạt lẫn nhau một cách không cố ý.
Code cuối cùng dài hơn bản gốc, nhưng tách các Effect này vẫn đúng. Mỗi Effect nên đại diện cho một quá trình đồng bộ độc lập. Trong ví dụ này, xóa một Effect không phá vỡ logic của Effect khác. Điều này có nghĩa là chúng đồng bộ những thứ khác nhau, và việc tách chúng ra là tốt. Nếu bạn lo lắng về việc trùng lặp, bạn có thể cải thiện code này bằng cách trích xuất logic lặp lại thành một custom Hook.
Bạn có đang đọc một số state để tính toán state tiếp theo không?
Effect này cập nhật biến state messages
với một array mới được tạo mỗi khi có tin nhắn mới đến:
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages([...messages, receivedMessage]);
});
// ...
Nó sử dụng biến messages
để tạo một array mới bắt đầu với tất cả các tin nhắn hiện có và thêm tin nhắn mới vào cuối. Tuy nhiên, vì messages
là một giá trị reactive được đọc bởi Effect, nó phải là một dependency:
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages([...messages, receivedMessage]);
});
return () => connection.disconnect();
}, [roomId, messages]); // ✅ All dependencies declared
// ...
Và việc làm messages
thành dependency gây ra một vấn đề.
Mỗi khi bạn nhận được tin nhắn, setMessages()
khiến component render lại với một array messages
mới bao gồm tin nhắn đã nhận. Tuy nhiên, vì Effect này bây giờ phụ thuộc vào messages
, điều này cũng sẽ đồng bộ lại Effect. Vì vậy, mỗi tin nhắn mới sẽ làm cho chat kết nối lại. Người dùng sẽ không thích điều đó!
Để sửa vấn đề, đừng đọc messages
bên trong Effect. Thay vào đó, truyền một updater function cho setMessages
:
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages(msgs => [...msgs, receivedMessage]);
});
return () => connection.disconnect();
}, [roomId]); // ✅ All dependencies declared
// ...
Lưu ý cách Effect của bạn không đọc biến messages
chút nào bây giờ. Bạn chỉ cần truyền một updater function như msgs => [...msgs, receivedMessage]
. React đặt updater function của bạn vào một hàng đợi và sẽ cung cấp tham số msgs
cho nó trong lần render tiếp theo. Đây là lý do tại sao bản thân Effect không cần phụ thuộc vào messages
nữa. Kết quả của việc sửa này, việc nhận tin nhắn chat sẽ không còn làm cho chat kết nối lại.
Bạn có muốn đọc một giá trị mà không “phản ứng” với những thay đổi của nó không?
Giả sử bạn muốn phát âm thanh khi người dùng nhận tin nhắn mới trừ khi isMuted
là true
:
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
const [isMuted, setIsMuted] = useState(false);
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages(msgs => [...msgs, receivedMessage]);
if (!isMuted) {
playSound();
}
});
// ...
Vì Effect của bạn bây giờ sử dụng isMuted
trong code, bạn phải thêm nó vào dependency:
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
const [isMuted, setIsMuted] = useState(false);
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages(msgs => [...msgs, receivedMessage]);
if (!isMuted) {
playSound();
}
});
return () => connection.disconnect();
}, [roomId, isMuted]); // ✅ All dependencies declared
// ...
Vấn đề là mỗi khi isMuted
thay đổi (ví dụ, khi người dùng nhấn nút “Muted”), Effect sẽ đồng bộ lại và kết nối lại với chat. Đây không phải là trải nghiệm người dùng mong muốn! (Trong ví dụ này, ngay cả việc vô hiệu hóa linter cũng không hoạt động—nếu bạn làm vậy, isMuted
sẽ bị “kẹt” với giá trị cũ.)
Để giải quyết vấn đề này, bạn cần trích xuất logic không nên là reactive ra khỏi Effect. Bạn không muốn Effect này “phản ứng” với những thay đổi trong isMuted
. Di chuyển đoạn logic không reactive này vào một Effect Event:
import { useState, useEffect, useEffectEvent } from 'react';
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
const [isMuted, setIsMuted] = useState(false);
const onMessage = useEffectEvent(receivedMessage => {
setMessages(msgs => [...msgs, receivedMessage]);
if (!isMuted) {
playSound();
}
});
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
onMessage(receivedMessage);
});
return () => connection.disconnect();
}, [roomId]); // ✅ All dependencies declared
// ...
Effect Event cho phép bạn chia một Effect thành các phần reactive (nên “phản ứng” với các giá trị reactive như roomId
và những thay đổi của chúng) và các phần không reactive (chỉ đọc các giá trị mới nhất của chúng, như onMessage
đọc isMuted
). Bây giờ bạn đọc isMuted
bên trong Effect Event, nó không cần phải là dependency của Effect. Kết quả là, chat sẽ không kết nối lại khi bạn bật/tắt cài đặt “Muted”, giải quyết vấn đề ban đầu!
Bao bọc event handler từ props
Bạn có thể gặp phải vấn đề tương tự khi component nhận event handler như một prop:
function ChatRoom({ roomId, onReceiveMessage }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
onReceiveMessage(receivedMessage);
});
return () => connection.disconnect();
}, [roomId, onReceiveMessage]); // ✅ All dependencies declared
// ...
Giả sử component cha truyền một function onReceiveMessage
khác trong mỗi lần render:
<ChatRoom
roomId={roomId}
onReceiveMessage={receivedMessage => {
// ...
}}
/>
Vì onReceiveMessage
là một dependency, nó sẽ khiến Effect đồng bộ lại sau mỗi lần component cha render lại. Điều này sẽ làm cho nó kết nối lại với chat. Để giải quyết điều này, hãy bao bọc cuộc gọi trong Effect Event:
function ChatRoom({ roomId, onReceiveMessage }) {
const [messages, setMessages] = useState([]);
const onMessage = useEffectEvent(receivedMessage => {
onReceiveMessage(receivedMessage);
});
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
onMessage(receivedMessage);
});
return () => connection.disconnect();
}, [roomId]); // ✅ All dependencies declared
// ...
Effect Event không phải là reactive, vì vậy bạn không cần chỉ định chúng làm dependency. Kết quả là, chat sẽ không còn kết nối lại ngay cả khi component cha truyền một function khác trong mỗi lần render lại.
Tách code reactive và không reactive
Trong ví dụ này, bạn muốn ghi log một lần visit mỗi khi roomId
thay đổi. Bạn muốn bao gồm notificationCount
hiện tại với mọi log, nhưng bạn không muốn một thay đổi trong notificationCount
kích hoạt sự kiện log.
Giải pháp một lần nữa là tách code không reactive vào Effect Event:
function Chat({ roomId, notificationCount }) {
const onVisit = useEffectEvent(visitedRoomId => {
logVisit(visitedRoomId, notificationCount);
});
useEffect(() => {
onVisit(roomId);
}, [roomId]); // ✅ All dependencies declared
// ...
}
Bạn muốn logic của mình reactive đối với roomId
, vì vậy bạn đọc roomId
bên trong Effect. Tuy nhiên, bạn không muốn thay đổi notificationCount
ghi log thêm visit, vì vậy bạn đọc notificationCount
bên trong Effect Event. Tìm hiểu thêm về việc đọc props và state mới nhất từ Effect bằng Effect Event.
Có giá trị reactive nào thay đổi một cách không cố ý không?
Đôi khi, bạn thực sự muốn Effect “phản ứng” với một giá trị nhất định, nhưng giá trị đó thay đổi thường xuyên hơn bạn muốn—và có thể không phản ánh bất kỳ thay đổi thực tế nào từ góc độ người dùng. Ví dụ, giả sử bạn tạo một object options
trong thân component của mình, và sau đó đọc object đó từ bên trong Effect:
function ChatRoom({ roomId }) {
// ...
const options = {
serverUrl: serverUrl,
roomId: roomId
};
useEffect(() => {
const connection = createConnection(options);
connection.connect();
// ...
Object này được khai báo trong thân component, vì vậy nó là một giá trị reactive. Khi bạn đọc một giá trị reactive như thế này bên trong Effect, bạn khai báo nó như một dependency. Điều này đảm bảo Effect của bạn “phản ứng” với những thay đổi của nó:
// ...
useEffect(() => {
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [options]); // ✅ All dependencies declared
// ...
Việc khai báo nó như một dependency là rất quan trọng! Điều này đảm bảo, ví dụ, nếu roomId
thay đổi, Effect của bạn sẽ kết nối lại với chat với options
mới. Tuy nhiên, cũng có một vấn đề với code ở trên. Để thấy điều đó, hãy thử gõ vào input trong sandbox bên dưới, và xem điều gì xảy ra trong console:
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; const serverUrl = 'https://localhost:1234'; function ChatRoom({ roomId }) { const [message, setMessage] = useState(''); // Temporarily disable the linter to demonstrate the problem // eslint-disable-next-line react-hooks/exhaustive-deps 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} /> </> ); }
Trong sandbox ở trên, input chỉ cập nhật biến state message
. Từ góc độ người dùng, điều này không nên ảnh hưởng đến kết nối chat. Tuy nhiên, mỗi khi bạn cập nhật message
, component của bạn sẽ render lại. Khi component render lại, code bên trong nó chạy lại từ đầu.
Một object options
mới được tạo từ đầu mỗi khi ChatRoom
component render lại. React thấy rằng object options
là một object khác với object options
được tạo trong lần render trước. Đây là lý do tại sao nó đồng bộ lại Effect của bạn (phụ thuộc vào options
), và chat kết nối lại khi bạn gõ.
Vấn đề này chỉ ảnh hưởng đến object và function. Trong JavaScript, mỗi object và function mới được tạo ra đều được coi là khác biệt với tất cả các object/function khác. Không quan trọng nội dung bên trong chúng có giống nhau hay không!
// During the first render
const options1 = { serverUrl: 'https://localhost:1234', roomId: 'music' };
// During the next render
const options2 = { serverUrl: 'https://localhost:1234', roomId: 'music' };
// These are two different objects!
console.log(Object.is(options1, options2)); // false
Các dependency là object và function có thể khiến Effect đồng bộ lại thường xuyên hơn bạn cần.
Đây là lý do tại sao, bất cứ khi nào có thể, bạn nên cố gắng tránh các object và function làm dependency của Effect. Thay vào đó, hãy thử di chuyển chúng ra ngoài component, vào trong Effect, hoặc trích xuất các giá trị nguyên thủy từ chúng.
Di chuyển các object và function tĩnh ra ngoài component
Nếu object không phụ thuộc vào bất kỳ props và state nào, bạn có thể di chuyển object đó ra ngoài component:
const options = {
serverUrl: 'https://localhost:1234',
roomId: 'music'
};
function ChatRoom() {
const [message, setMessage] = useState('');
useEffect(() => {
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, []); // ✅ All dependencies declared
// ...
Bằng cách này, bạn chứng minh cho linter rằng nó không reactive. Nó không thể thay đổi do kết quả của việc render lại, vì vậy nó không cần phải là một dependency. Bây giờ việc render lại ChatRoom
sẽ không khiến Effect của bạn đồng bộ lại.
Điều này cũng hoạt động cho function:
function createOptions() {
return {
serverUrl: 'https://localhost:1234',
roomId: 'music'
};
}
function ChatRoom() {
const [message, setMessage] = useState('');
useEffect(() => {
const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, []); // ✅ All dependencies declared
// ...
Vì createOptions
được khai báo bên ngoài component của bạn, nó không phải là một giá trị reactive. Đây là lý do tại sao nó không cần được chỉ định trong các dependency của Effect, và tại sao nó sẽ không bao giờ khiến Effect của bạn đồng bộ lại.
Di chuyển các object và function động vào trong Effect
Nếu object của bạn phụ thuộc vào một số giá trị reactive có thể thay đổi do kết quả của việc render lại, như một prop roomId
, bạn không thể kéo nó ra bên ngoài component. Tuy nhiên, bạn có thể di chuyển việc tạo nó vào trong code Effect của bạn:
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]); // ✅ All dependencies declared
// ...
Bây giờ options
được khai báo bên trong Effect của bạn, nó không còn là dependency của Effect nữa. Thay vào đó, giá trị reactive duy nhất được sử dụng bởi Effect là roomId
. Vì roomId
không phải là object hoặc function, bạn có thể chắc chắn rằng nó sẽ không vô tình khác biệt. Trong JavaScript, number và string được so sánh theo nội dung của chúng:
// During the first render
const roomId1 = 'music';
// During the next render
const roomId2 = 'music';
// These two strings are the same!
console.log(Object.is(roomId1, roomId2)); // true
Nhờ vào sửa chữa này, chat không còn kết nối lại nếu 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(''); 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} /> </> ); }
Tuy nhiên, nó có kết nối lại khi bạn thay đổi dropdown roomId
, như bạn mong đợi.
Điều này cũng hoạt động với các function:
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]); // ✅ All dependencies declared
// ...
Bạn có thể viết các function của riêng mình để nhóm các phần logic bên trong Effect. Miễn là bạn cũng khai báo chúng bên trong Effect, chúng không phải là giá trị reactive, và vì vậy chúng không cần phải là dependency của Effect.
Đọc các giá trị nguyên thủy từ object
Đôi khi, bạn có thể nhận một object từ props:
function ChatRoom({ options }) {
const [message, setMessage] = useState('');
useEffect(() => {
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [options]); // ✅ All dependencies declared
// ...
Rủi ro ở đây là component cha sẽ tạo object trong quá trình rendering:
<ChatRoom
roomId={roomId}
options={{
serverUrl: serverUrl,
roomId: roomId
}}
/>
Điều này sẽ khiến Effect của bạn kết nối lại mỗi khi component cha render lại. Để sửa điều này, hãy đọc thông tin từ object bên ngoài Effect, và tránh có các dependency là object và function:
function ChatRoom({ options }) {
const [message, setMessage] = useState('');
const { roomId, serverUrl } = options;
useEffect(() => {
const connection = createConnection({
roomId: roomId,
serverUrl: serverUrl
});
connection.connect();
return () => connection.disconnect();
}, [roomId, serverUrl]); // ✅ All dependencies declared
// ...
Logic trở nên hơi lặp lại (bạn đọc một số giá trị từ object bên ngoài Effect, và sau đó tạo một object với các giá trị giống nhau bên trong Effect). Nhưng nó làm cho việc Effect của bạn thực sự phụ thuộc vào thông tin gì trở nên rất rõ ràng. Nếu một object được tạo lại một cách không cố ý bởi component cha, chat sẽ không kết nối lại. Tuy nhiên, nếu options.roomId
hoặc options.serverUrl
thực sự khác, chat sẽ kết nối lại.
Tính toán các giá trị nguyên thủy từ function
Cách tiếp cận tương tự có thể hoạt động cho function. Ví dụ, giả sử component cha truyền một function:
<ChatRoom
roomId={roomId}
getOptions={() => {
return {
serverUrl: serverUrl,
roomId: roomId
};
}}
/>
Để tránh làm cho nó trở thành dependency (và khiến nó kết nối lại khi render lại), hãy gọi nó bên ngoài Effect. Điều này cung cấp cho bạn các giá trị roomId
và serverUrl
không phải là object, và bạn có thể đọc từ bên trong Effect:
function ChatRoom({ getOptions }) {
const [message, setMessage] = useState('');
const { roomId, serverUrl } = getOptions();
useEffect(() => {
const connection = createConnection({
roomId: roomId,
serverUrl: serverUrl
});
connection.connect();
return () => connection.disconnect();
}, [roomId, serverUrl]); // ✅ All dependencies declared
// ...
Điều này chỉ hoạt động cho các function thuần khiết vì chúng an toàn để gọi trong quá trình rendering. Nếu function của bạn là một event handler, nhưng bạn không muốn những thay đổi của nó đồng bộ lại Effect, hãy bao bọc nó vào một Effect Event thay thế.
Tóm tắt
- Các dependency nên luôn khớp với code.
- Khi bạn không hài lòng với các dependency, điều bạn cần chỉnh sửa là code.
- Việc bỏ qua linter dẫn đến những bug rất khó hiểu, và bạn nên luôn tránh điều đó.
- Để loại bỏ một dependency, bạn cần “chứng minh” cho linter rằng nó không cần thiết.
- Nếu một số code nên chạy để phản hồi một tương tác cụ thể, hãy di chuyển code đó vào một event handler.
- Nếu các phần khác nhau của Effect nên chạy lại vì những lý do khác nhau, hãy chia nó thành nhiều Effect.
- Nếu bạn muốn cập nhật một số state dựa trên state trước đó, hãy truyền một updater function.
- Nếu bạn muốn đọc giá trị mới nhất mà không “phản ứng” với nó, hãy trích xuất một Effect Event từ Effect của bạn.
- Trong JavaScript, các object và function được coi là khác nhau nếu chúng được tạo ra ở những thời điểm khác nhau.
- Hãy cố gắng tránh các dependency là object và function. Di chuyển chúng ra ngoài component hoặc vào trong Effect.
Challenge 1 of 4: Sửa interval bị reset
Effect này thiết lập một interval tick mỗi giây. Bạn nhận thấy có điều gì đó lạ xảy ra: có vẻ như interval bị hủy và tạo lại mỗi khi nó tick. Hãy sửa code để interval không bị tạo lại liên tục.
import { useState, useEffect } from 'react'; export default function Timer() { const [count, setCount] = useState(0); useEffect(() => { console.log('✅ Creating an interval'); const id = setInterval(() => { console.log('⏰ Interval tick'); setCount(count + 1); }, 1000); return () => { console.log('❌ Clearing an interval'); clearInterval(id); }; }, [count]); return <h1>Counter: {count}</h1> }