Vòng đời của Reactive Effects

Effect có một vòng đời khác với các component. Các 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 một thứ 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. React cung cấp một quy tắc linter để kiểm tra rằng bạn đã chỉ định đúng các dependency của Effect. Điều này giữ cho Effect của bạn được đồng bộ hóa với props và state mới nhất.

Bạn sẽ được học

  • Effect có vòng đời khác với vòng đời của component như thế nào
  • Cách suy nghĩ về từng Effect riêng lẻ một cách độc lập
  • Khi nào Effect của bạn cần đồng bộ hóa lại, và tại sao
  • Cách xác định các dependency của Effect
  • Ý nghĩa của một giá trị reactive là gì
  • Ý nghĩa của một mảng dependency trống
  • React xác minh các dependency của bạn đúng với linter như thế nào
  • Làm gì khi bạn không đồng ý với linter

Vòng đời của một Effect

Mọi React component đều trải qua cùng một vòng đời:

  • Một component mount khi nó được thêm vào màn hình.
  • Một component update khi nó nhận được props hoặc state mới, thường để phản hồi lại một tương tác.
  • Một component unmount khi nó được loại bỏ khỏi màn hình.

Đó là một cách tốt để nghĩ về component, nhưng không phải về Effect. Thay vào đó, hãy cố gắng nghĩ về từng Effect một cách độc lập với vòng đời của component. Một Effect mô tả cách đồng bộ hóa một hệ thống bên ngoài với props và state hiện tại. Khi code của bạn thay đổi, việc đồng bộ hóa sẽ cần xảy ra nhiều hơn hoặc ít hơn.

Để minh họa điểm này, hãy xem xét Effect này kết nối component của bạn với một chat server:

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]);
// ...
}

Body của Effect chỉ định cách bắt đầu đồng bộ hóa:

// ...
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
// ...

Function cleanup được trả về bởi Effect chỉ định cách ngừng đồng bộ hóa:

// ...
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
// ...

Theo trực giác, bạn có thể nghĩ rằng React sẽ bắt đầu đồng bộ hóa khi component mount và ngừng đồng bộ hóa khi component unmount. Tuy nhiên, đây không phải là kết thúc của câu chuyện! Đôi khi, cũng có thể cần bắt đầu và ngừng đồng bộ hóa nhiều lần trong khi component vẫn được mount.

Hãy xem tại sao điều này cần thiết, khi nào nó xảy ra, và cách bạn có thể kiểm soát hành vi này.

Note

Một số Effect không trả về function cleanup nào cả. Thông thường, bạn sẽ muốn trả về một cái—nhưng nếu bạn không làm vậy, React sẽ hoạt động như thể bạn đã trả về một function cleanup rỗng.

Tại sao việc đồng bộ hóa có thể cần xảy ra nhiều lần

Hãy tưởng tượng component ChatRoom này nhận một prop roomId mà người dùng chọn trong một dropdown. Giả sử ban đầu người dùng chọn phòng "general" làm roomId. Ứng dụng của bạn hiển thị phòng chat "general":

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId /* "general" */ }) {
// ...
return <h1>Welcome to the {roomId} room!</h1>;
}

Sau khi UI được hiển thị, React sẽ chạy Effect để bắt đầu đồng bộ hóa. Nó kết nối với phòng "general":

function ChatRoom({ roomId /* "general" */ }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Connects to the "general" room
connection.connect();
return () => {
connection.disconnect(); // Disconnects from the "general" room
};
}, [roomId]);
// ...

Tới đây, mọi thứ vẫn ổn.

Sau đó, người dùng chọn một phòng khác trong dropdown (ví dụ, "travel"). Trước tiên, React sẽ cập nhật UI:

function ChatRoom({ roomId /* "travel" */ }) {
// ...
return <h1>Welcome to the {roomId} room!</h1>;
}

Hãy nghĩ về những gì sẽ xảy ra tiếp theo. Người dùng thấy rằng "travel" là phòng chat được chọn trong UI. Tuy nhiên, Effect đã chạy lần cuối vẫn còn kết nối với phòng "general". Prop roomId đã thay đổi, vì vậy những gì Effect của bạn đã làm trước đó (kết nối với phòng "general") không còn khớp với UI nữa.

Tại thời điểm này, bạn muốn React thực hiện hai việc:

  1. Ngừng đồng bộ hóa với roomId cũ (ngắt kết nối khỏi phòng "general")
  2. Bắt đầu đồng bộ hóa với roomId mới (kết nối với phòng "travel")

May mắn thay, bạn đã dạy React cách thực hiện cả hai việc này! Body Effect của bạn chỉ định cách bắt đầu đồng bộ hóa, và function cleanup chỉ định cách ngừng đồng bộ hóa. Tất cả những gì React cần làm bây giờ là gọi chúng theo đúng thứ tự và với props và state đúng. Hãy xem chính xác điều đó xảy ra như thế nào.

React đồng bộ hóa lại Effect của bạn như thế nào

Hãy nhớ lại rằng component ChatRoom của bạn đã nhận một giá trị mới cho prop roomId. Trước đó là "general", và bây giờ là "travel". React cần đồng bộ hóa lại Effect để kết nối lại bạn với một phòng khác.

Để ngừng đồng bộ hóa, React sẽ gọi function cleanup mà Effect của bạn trả về sau khi kết nối với phòng "general". Vì roomId"general", function cleanup ngắt kết nối khỏi phòng "general":

function ChatRoom({ roomId /* "general" */ }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Connects to the "general" room
connection.connect();
return () => {
connection.disconnect(); // Disconnects from the "general" room
};
// ...

Sau đó React sẽ chạy Effect mà bạn đã cung cấp trong lần render này. Lần này, roomId"travel" nên nó sẽ bắt đầu đồng bộ hóa với phòng chat "travel" (cho đến khi function cleanup của nó cũng được gọi):

function ChatRoom({ roomId /* "travel" */ }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Connects to the "travel" room
connection.connect();
// ...

Nhờ điều này, bây giờ bạn đã kết nối với cùng phòng mà người dùng đã chọn trong UI. Tránh được thảm họa!

Mỗi lần sau khi component render lại với một roomId khác, Effect của bạn sẽ đồng bộ hóa lại. Ví dụ, giả sử người dùng thay đổi roomId từ "travel" thành "music". React sẽ lại ngừng đồng bộ hóa Effect của bạn bằng cách gọi function cleanup (ngắt kết nối bạn khỏi phòng "travel"). Sau đó nó sẽ bắt đầu đồng bộ hóa lại bằng cách chạy body với prop roomId mới (kết nối bạn với phòng "music").

Cuối cùng, khi người dùng chuyển sang một màn hình khác, ChatRoom unmount. Bây giờ không cần phải duy trì kết nối nữa. React sẽ ngừng đồng bộ hóa Effect của bạn lần cuối cùng và ngắt kết nối bạn khỏi phòng chat "music".

Suy nghĩ từ góc độ của Effect

Hãy tóm tắt mọi thứ đã xảy ra từ góc độ của component ChatRoom:

  1. ChatRoom mount với roomId được đặt thành "general"
  2. ChatRoom update với roomId được đặt thành "travel"
  3. ChatRoom update với roomId được đặt thành "music"
  4. ChatRoom unmount

Trong mỗi điểm này trong vòng đời của component, Effect của bạn đã làm những việc khác nhau:

  1. Effect của bạn kết nối với phòng "general"
  2. Effect của bạn ngắt kết nối khỏi phòng "general" và kết nối với phòng "travel"
  3. Effect của bạn ngắt kết nối khỏi phòng "travel" và kết nối với phòng "music"
  4. Effect của bạn ngắt kết nối khỏi phòng "music"

Bây giờ hãy nghĩ về những gì đã xảy ra từ góc độ của chính Effect:

useEffect(() => {
// Your Effect connected to the room specified with roomId...
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
// ...until it disconnected
connection.disconnect();
};
}, [roomId]);

Cấu trúc của code này có thể truyền cảm hứng cho bạn để thấy những gì đã xảy ra như một chuỗi các khoảng thời gian không chồng chéo:

  1. Effect của bạn kết nối với phòng "general" (cho đến khi nó ngắt kết nối)
  2. Effect của bạn kết nối với phòng "travel" (cho đến khi nó ngắt kết nối)
  3. Effect của bạn kết nối với phòng "music" (cho đến khi nó ngắt kết nối)

Trước đây, bạn đang suy nghĩ từ góc độ của component. Khi bạn nhìn từ góc độ của component, có thể dễ dàng nghĩ về Effect như “callback” hoặc “lifecycle event” được kích hoạt tại một thời điểm như “sau một lần render” hoặc “trước khi unmount”. Cách suy nghĩ này trở nên phức tạp rất nhanh, vì vậy tốt nhất là tránh.

Thay vào đó, hãy luôn tập trung vào một chu kỳ bắt đầu/dừng duy nhất tại một thời điểm. Không quan trọng liệu component đang mount, update, hay unmount. Tất cả những gì bạn cần làm là mô tả cách bắt đầu đồng bộ hóa và cách dừng nó. Nếu bạn làm tốt, Effect của bạn sẽ chịu đựng được việc được bắt đầu và dừng bao nhiêu lần cần thiết.

Điều này có thể nhắc nhở bạn về cách bạn không nghĩ liệu component đang mount hoặc update khi bạn viết logic rendering tạo JSX. Bạn mô tả những gì nên có trên màn hình, và React tìm ra phần còn lại.

React xác minh rằng Effect của bạn có thể đồng bộ hóa lại như thế nào

Đây là một ví dụ trực tiếp mà bạn có thể chơi với. Nhấn “Open chat” để mount component ChatRoom:

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');
  const [show, setShow] = 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>
      <button onClick={() => setShow(!show)}>
        {show ? 'Close chat' : 'Open chat'}
      </button>
      {show && <hr />}
      {show && <ChatRoom roomId={roomId} />}
    </>
  );
}

Lưu ý rằng khi component mount lần đầu tiên, bạn thấy ba log:

  1. ✅ Connecting to "general" room at https://localhost:1234... (chỉ trong development)
  2. ❌ Disconnected from "general" room at https://localhost:1234. (chỉ trong development)
  3. ✅ Connecting to "general" room at https://localhost:1234...

Hai log đầu tiên chỉ dành cho development. Trong development, React luôn remount mỗi component một lần.

React xác minh rằng Effect của bạn có thể đồng bộ hóa lại bằng cách buộc nó thực hiện điều đó ngay lập tức trong development. Điều này có thể nhắc nhở bạn về việc mở cửa và đóng nó thêm một lần để kiểm tra xem ổ khóa cửa có hoạt động không. React bắt đầu và dừng Effect của bạn thêm một lần trong development để kiểm tra bạn đã triển khai function cleanup tốt chưa.

Lý do chính Effect của bạn sẽ đồng bộ hóa lại trong thực tế là nếu một số data mà nó sử dụng đã thay đổi. Trong sandbox ở trên, hãy thay đổi phòng chat được chọn. Lưu ý cách, khi roomId thay đổi, Effect của bạn đồng bộ hóa lại.

Tuy nhiên, cũng có những trường hợp bất thường hơn mà việc đồng bộ hóa lại là cần thiết. Ví dụ, hãy thử chỉnh sửa serverUrl trong sandbox ở trên trong khi chat đang mở. Lưu ý cách Effect đồng bộ hóa lại để phản hồi lại các chỉnh sửa của bạn đối với code. Trong tương lai, React có thể thêm nhiều tính năng hơn dựa trên việc đồng bộ hóa lại.

React biết rằng nó cần đồng bộ hóa lại Effect như thế nào

Bạn có thể tự hỏi React biết rằng Effect của bạn cần đồng bộ hóa lại sau khi roomId thay đổi như thế nào. Đó là bởi vì bạn đã nói với React rằng code của nó phụ thuộc vào roomId bằng cách bao gồm nó trong danh sách dependency:

function ChatRoom({ roomId }) { // The roomId prop may change over time
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // This Effect reads roomId
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]); // So you tell React that this Effect "depends on" roomId
// ...

Đây là cách hoạt động của nó:

  1. Bạn biết roomId là một prop, có nghĩa là nó có thể thay đổi theo thời gian.
  2. Bạn biết rằng Effect đọc roomId (do đó logic của nó phụ thuộc vào giá trị có thể thay đổi sau này).
  3. Đây là lý do tại sao bạn chỉ định nó như dependency của Effect (để nó đồng bộ hóa lại khi roomId thay đổi).

Mỗi lần sau khi component render lại, React sẽ xem xét mảng các dependency mà bạn đã truyền. Nếu bất kỳ giá trị nào trong mảng khác với giá trị tại cùng vị trí mà bạn đã truyền trong lần render trước, React sẽ đồng bộ hóa lại Effect của bạn.

Ví dụ, nếu bạn truyền ["general"] trong lần render đầu tiên, và sau đó bạn truyền ["travel"] trong lần render tiếp theo, React sẽ so sánh "general""travel". Đây là những giá trị khác nhau (so sánh với Object.is), do đó React sẽ đồng bộ hóa lại Effect của bạn. Mặt khác, nếu component render lại nhưng roomId không thay đổi, Effect của bạn sẽ vẫn kết nối với cùng phòng.

Mỗi Effect đại diện cho một quá trình đồng bộ hóa riêng biệt

Hãy tránh thêm logic không liên quan vào Effect của bạn chỉ vì logic này cần chạy cùng lúc với Effect mà bạn đã viết. Ví dụ, giả sử bạn muốn gửi một sự kiện analytics khi người dùng truy cập phòng. Bạn đã có một Effect phụ thuộc vào roomId, vì vậy bạn có thể muốn thêm cuộc gọi analytics vào đó:

function ChatRoom({ roomId }) {
useEffect(() => {
logVisit(roomId);
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]);
// ...
}

Nhưng hãy tưởng tượng sau này bạn thêm một dependency khác vào Effect này mà cần thiết lập lại kết nối. Nếu Effect này đồng bộ hóa lại, nó cũng sẽ gọi logVisit(roomId) cho cùng phòng, điều mà bạn không có ý định. Ghi log lần truy cập là một quá trình riêng biệt so với việc kết nối. Viết chúng như hai Effect riêng biệt:

function ChatRoom({ roomId }) {
useEffect(() => {
logVisit(roomId);
}, [roomId]);

useEffect(() => {
const connection = createConnection(serverUrl, roomId);
// ...
}, [roomId]);
// ...
}

Mỗi Effect trong code của bạn nên đại diện cho một quá trình đồng bộ hóa riêng biệt và độc lập.

Trong ví dụ trên, việc xóa một Effect sẽ không làm hỏng logic của Effect khác. Đây là dấu hiệu tốt cho thấy chúng đồng bộ hóa những thứ khác nhau, và vì vậy việc tách chúng ra là hợp lý. Mặt khác, nếu bạn tách một phần logic gắn kết thành các Effect riêng biệt, code có thể trông “sạch sẽ” hơn nhưng sẽ khó duy trì hơn. Đây là lý do tại sao bạn nên suy nghĩ xem các quá trình có giống nhau hay riêng biệt, chứ không phải xem code có trông sạch hơn hay không.

Effect “phản ứng” với các giá trị reactive

Effect của bạn đọc hai biến (serverUrlroomId), nhưng bạn chỉ chỉ định roomId làm dependency:

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]);
// ...
}

Tại sao serverUrl không cần là một dependency?

Điều này là vì serverUrl không bao giờ thay đổi do việc render lại. Nó luôn giống nhau bất kể component render lại bao nhiêu lần và vì lý do gì. Vì serverUrl không bao giờ thay đổi, việc chỉ định nó làm dependency sẽ không có ý nghĩa. Xét cho cùng, dependency chỉ có tác dụng khi chúng thay đổi theo thời gian!

Mặt khác, roomId có thể khác trong lần render lại. Props, state, và các giá trị khác được khai báo bên trong component là reactive vì chúng được tính toán trong quá trình rendering và tham gia vào luồng data của React.

Nếu serverUrl là một biến state, nó sẽ là reactive. Các giá trị reactive phải được bao gồm trong dependency:

function ChatRoom({ roomId }) { // Props change over time
const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // State may change over time

useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Your Effect reads props and state
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId, serverUrl]); // So you tell React that this Effect "depends on" on props and state
// ...
}

Bằng cách bao gồm serverUrl làm dependency, bạn đảm bảo rằng Effect đồng bộ hóa lại sau khi nó thay đổi.

Hãy thử thay đổi phòng chat được chọn hoặc chỉnh sửa URL server trong sandbox này:

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>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} />
    </>
  );
}

Bất cứ khi nào bạn thay đổi một giá trị reactive như roomId hoặc serverUrl, Effect sẽ kết nối lại với chat server.

Ý nghĩa của một Effect với dependency rỗng

Điều gì xảy ra nếu bạn di chuyển cả serverUrlroomId ra ngoài component?

const serverUrl = 'https://localhost:1234';
const roomId = 'general';

function ChatRoom() {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, []); // ✅ All dependencies declared
// ...
}

Bây giờ code Effect của bạn không sử dụng bất kỳ giá trị reactive nào, vì vậy dependencies của nó có thể rỗng ([]).

Suy nghĩ từ góc độ của component, mảng dependency rỗng [] có nghĩa là Effect này kết nối với phòng chat chỉ khi component mount, và ngắt kết nối chỉ khi component unmount. (Hãy nhớ rằng React vẫn sẽ đồng bộ hóa lại nó thêm một lần nữa trong development để kiểm tra logic của bạn.)

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

const serverUrl = 'https://localhost:1234';
const roomId = 'general';

function ChatRoom() {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, []);
  return <h1>Welcome to the {roomId} room!</h1>;
}

export default function App() {
  const [show, setShow] = useState(false);
  return (
    <>
      <button onClick={() => setShow(!show)}>
        {show ? 'Close chat' : 'Open chat'}
      </button>
      {show && <hr />}
      {show && <ChatRoom />}
    </>
  );
}

Tuy nhiên, nếu bạn suy nghĩ từ góc độ của Effect, bạn không cần nghĩ về mounting và unmounting chút nào. Điều quan trọng là bạn đã chỉ định những gì Effect của bạn làm để bắt đầu và dừng đồng bộ hóa. Hôm nay, nó không có dependencies reactive. Nhưng nếu bạn muốn người dùng thay đổi roomId hoặc serverUrl theo thời gian (và chúng sẽ trở thành reactive), code Effect của bạn sẽ không thay đổi. Bạn sẽ chỉ cần thêm chúng vào dependencies.

Tất cả các biến được khai báo trong body component đều là reactive

Props và state không phải là những giá trị reactive duy nhất. Các giá trị mà bạn tính toán từ chúng cũng là reactive. Nếu props hoặc state thay đổi, component của bạn sẽ render lại, và các giá trị được tính toán từ chúng cũng sẽ thay đổi. Đây là lý do tại sao tất cả các biến từ body component được sử dụng bởi Effect nên có trong danh sách dependency của Effect.

Giả sử người dùng có thể chọn một chat server trong dropdown, nhưng họ cũng có thể cấu hình một server mặc định trong cài đặt. Giả sử bạn đã đặt state cài đặt trong một context để bạn đọc settings từ context đó. Bây giờ bạn tính toán serverUrl dựa trên server được chọn từ props và server mặc định:

function ChatRoom({ roomId, selectedServerUrl }) { // roomId is reactive
const settings = useContext(SettingsContext); // settings is reactive
const serverUrl = selectedServerUrl ?? settings.defaultServerUrl; // serverUrl is reactive
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Your Effect reads roomId and serverUrl
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId, serverUrl]); // So it needs to re-synchronize when either of them changes!
// ...
}

Trong ví dụ này, serverUrl không phải là một prop hoặc biến state. Nó là một biến thông thường mà bạn tính toán trong quá trình rendering. Nhưng nó được tính toán trong quá trình rendering, vì vậy nó có thể thay đổi do việc render lại. Đây là lý do tại sao nó là reactive.

Tất cả các giá trị bên trong component (bao gồm props, state, và các biến trong body component của bạn) đều là reactive. Bất kỳ giá trị reactive nào cũng có thể thay đổi trong lần render lại, vì vậy bạn cần bao gồm các giá trị reactive làm dependencies của Effect.

Nói cách khác, Effect “phản ứng” với tất cả các giá trị từ body component.

Tìm hiểu sâu

Các giá trị global hoặc mutable có thể là dependencies không?

Các giá trị mutable (bao gồm các biến global) không phải là reactive.

Một giá trị mutable như location.pathname không thể là một dependency. Nó có thể thay đổi (mutable), vì vậy nó có thể thay đổi bất cứ lúc nào hoàn toàn bên ngoài luồng data rendering của React. Việc thay đổi nó sẽ không kích hoạt render lại component của bạn. Do đó, ngay cả khi bạn chỉ định nó trong dependencies, React sẽ không biết để đồng bộ hóa lại Effect khi nó thay đổi. Điều này cũng vi phạm các quy tắc của React vì việc đọc data mutable trong quá trình rendering (là lúc bạn tính toán dependencies) vi phạm tính thuần khiết của rendering. Thay vào đó, bạn nên đọc và subscribe vào một giá trị mutable bên ngoài với useSyncExternalStore.

Một giá trị mutable như ref.current hoặc những thứ bạn đọc từ nó cũng không thể là một dependency. Object ref được trả về bởi useRef có thể là một dependency, nhưng thuộc tính current của nó có thể thay đổi một cách có chủ ý. Nó cho phép bạn theo dõi một thứ gì đó mà không kích hoạt render lại. Nhưng vì việc thay đổi nó không kích hoạt render lại, nó không phải là một giá trị reactive, và React sẽ không biết để chạy lại Effect của bạn khi nó thay đổi.

Như bạn sẽ học bên dưới trang này, một linter sẽ kiểm tra những vấn đề này một cách tự động.

React xác minh rằng bạn đã chỉ định mọi giá trị reactive làm dependency

Nếu linter của bạn được cấu hình cho React, nó sẽ kiểm tra rằng mọi giá trị reactive được sử dụng bởi code Effect của bạn đều được khai báo làm dependency của nó. Ví dụ, đây là một lỗi lint vì cả roomIdserverUrl đều là reactive:

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

function ChatRoom({ roomId }) { // roomId is reactive
  const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // serverUrl is reactive

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, []); // <-- Something's wrong here!

  return (
    <>
      <label>
        Server URL:{' '}
        <input
          value={serverUrl}
          onChange={e => setServerUrl(e.target.value)}
        />
      </label>
      <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ều này có thể trông giống như một lỗi React, nhưng thực sự React đang chỉ ra một bug trong code của bạn. Cả roomIdserverUrl đều có thể thay đổi theo thời gian, nhưng bạn đang quên đồng bộ hóa lại Effect của bạn khi chúng thay đổi. Bạn sẽ vẫn kết nối với roomIdserverUrl ban đầu ngay cả sau khi người dùng chọn các giá trị khác trong UI.

Để sửa bug, hãy làm theo gợi ý của linter để chỉ định roomIdserverUrl làm dependencies của Effect:

function ChatRoom({ roomId }) { // roomId is reactive
const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // serverUrl is reactive
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]); // ✅ All dependencies declared
// ...
}

Hãy thử sửa lỗi này trong sandbox ở trên. Xác minh rằng lỗi linter đã biến mất, và chat kết nối lại khi cần thiết.

Note

Trong một số trường hợp, React biết rằng một giá trị không bao giờ thay đổi mặc dù nó được khai báo bên trong component. Ví dụ, function set được trả về từ useState và object ref được trả về bởi useRefổn định—chúng được đảm bảo không thay đổi trong lần render lại. Các giá trị ổn định không phải là reactive, vì vậy bạn có thể bỏ qua chúng khỏi danh sách. Việc bao gồm chúng cũng được cho phép: chúng sẽ không thay đổi, vì vậy không quan trọng.

Làm gì khi bạn không muốn đồng bộ hóa lại

Trong ví dụ trước, bạn đã sửa lỗi lint bằng cách liệt kê roomIdserverUrl làm dependencies.

Tuy nhiên, thay vào đó bạn có thể “chứng minh” với linter rằng những giá trị này không phải là giá trị reactive, tức là chúng không thể thay đổi do việc render lại. Ví dụ, nếu serverUrlroomId không phụ thuộc vào rendering và luôn có cùng giá trị, bạn có thể di chuyển chúng ra ngoài component. Bây giờ chúng không cần phải là dependencies:

const serverUrl = 'https://localhost:1234'; // serverUrl is not reactive
const roomId = 'general'; // roomId is not reactive

function ChatRoom() {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, []); // ✅ All dependencies declared
// ...
}

Bạn cũng có thể di chuyển chúng vào bên trong Effect. Chúng không được tính toán trong quá trình rendering, vì vậy chúng không phải là reactive:

function ChatRoom() {
useEffect(() => {
const serverUrl = 'https://localhost:1234'; // serverUrl is not reactive
const roomId = 'general'; // roomId is not reactive
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, []); // ✅ All dependencies declared
// ...
}

Effect là những khối code reactive. Chúng đồng bộ hóa lại khi các giá trị bạn đọc bên trong chúng thay đổi. Không giống như event handler chỉ chạy một lần cho mỗi tương tác, Effect chạy bất cứ khi nào việc đồng bộ hóa là cần thiết.

Bạn không thể “chọn” dependencies của mình. Dependencies của bạn phải bao gồm mọi giá trị reactive mà bạn đọc trong Effect. Linter thực thi điều này. Đôi khi điều này có thể dẫn đến các vấn đề như vòng lặp vô hạn và làm cho Effect của bạn đồng bộ hóa lại quá thường xuyên. Đừng sửa những vấn đề này bằng cách loại bỏ linter! Thay vào đó hãy thử những điều sau:

  • Kiểm tra rằng Effect của bạn đại diện cho một quá trình đồng bộ hóa độc lập. Nếu Effect của bạn không đồng bộ hóa bất cứ thứ gì, nó có thể không cần thiết. Nếu nó đồng bộ hóa nhiều thứ độc lập, hãy tách nó ra.

  • Nếu bạn muốn đọc giá trị mới nhất của props hoặc state mà không “phản ứng” với nó và đồng bộ hóa lại Effect, bạn có thể tách Effect của mình thành một phần reactive (mà bạn sẽ giữ trong Effect) và một phần non-reactive (mà bạn sẽ trích xuất thành thứ được gọi là Effect Event). Đọc về việc tách Events khỏi Effects.

  • Tránh dựa vào các object và function làm dependencies. Nếu bạn tạo các object và function trong quá trình rendering và sau đó đọc chúng từ Effect, chúng sẽ khác nhau trong mỗi lần render. Điều này sẽ khiến Effect của bạn đồng bộ hóa lại mỗi lần. Đọc thêm về việc loại bỏ các dependencies không cần thiết khỏi Effects.

Chú Ý

Linter là bạn của bạn, nhưng sức mạnh của nó có hạn. Linter chỉ biết khi nào dependencies sai. Nó không biết cách tốt nhất để giải quyết từng trường hợp. Nếu linter gợi ý một dependency, nhưng việc thêm nó gây ra vòng lặp, điều đó không có nghĩa là linter nên bị bỏ qua. Bạn cần thay đổi code bên trong (hoặc bên ngoài) Effect để giá trị đó không phải là reactive và không cần phải là dependency.

Nếu bạn có một codebase hiện có, bạn có thể có một số Effect loại bỏ linter như thế này:

useEffect(() => {
// ...
// 🔴 Avoid suppressing the linter like this:
// eslint-ignore-next-line react-hooks/exhaustive-deps
}, []);

Trên trang tiếp theo, bạn sẽ học cách sửa code này mà không vi phạm các quy tắc. Việc sửa luôn đáng giá!

Tóm tắt

  • Các component có thể mount, update, và unmount.
  • Mỗi Effect có một vòng đời riêng biệt với component xung quanh.
  • Mỗi Effect mô tả một quá trình đồng bộ hóa riêng biệt có thể bắt đầudừng.
  • Khi bạn viết và đọc Effect, hãy suy nghĩ từ góc độ của từng Effect riêng lẻ (cách bắt đầu và dừng đồng bộ hóa) thay vì từ góc độ của component (cách nó mount, update, hoặc unmount).
  • Các giá trị được khai báo bên trong body component là “reactive”.
  • Các giá trị reactive nên đồng bộ hóa lại Effect vì chúng có thể thay đổi theo thời gian.
  • Linter xác minh rằng tất cả các giá trị reactive được sử dụng bên trong Effect đều được chỉ định làm dependencies.
  • Tất cả các lỗi được linter đánh dấu đều hợp lệ. Luôn có cách để sửa code để không vi phạm các quy tắc.

Challenge 1 of 5:
Sửa lỗi kết nối lại sau mỗi phím gõ

Trong ví dụ này, component ChatRoom kết nối với phòng chat khi component mount, ngắt kết nối khi nó unmount, và kết nối lại khi bạn chọn một phòng chat khác. Hành vi này là đúng, vì vậy bạn cần giữ nó hoạt động.

Tuy nhiên, có một vấn đề. Bất cứ khi nào bạn gõ vào ô input tin nhắn ở phía dưới, ChatRoom cũng kết nối lại với chat. (Bạn có thể nhận thấy điều này bằng cách xóa console và gõ vào input.) Hãy sửa vấn đề để điều này không xảy ra.

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  });

  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} />
    </>
  );
}