Hiểu rõ hơn về useEffect trong React

Hiểu về thứ tự thực thi của useEffect trong React

Hiểu về thứ tự thực thi của useEffect trong React

useEffect là một trong những hooks được sử dụng phổ biến nhất trong cộng đồng React. Dù bạn có bao nhiêu kinh nghiệm với React, chắc chắn bạn đã từng sử dụng nó.

Nhưng bạn có bao giờ gặp tình huống mà các hook useEffect chạy theo thứ tự không như mong đợi khi nhiều lớp component được liên quan không?

Bắt đầu với một câu đố nhỏ

Thứ tự đúng của các câu lệnh console.log trong console là gì?

function Parent({ children }) {
  console.log("Parent is rendered");
  useEffect(() => {
    console.log("Parent committed effect");
  }, []);

  return <div>{children}</div>;
}

function Child() {
  console.log("Child is rendered");
  useEffect(() => {
    console.log("Child committed effect");
  }, []);

  return <p>Child</p>;
}

export default function App() {
  return (
    <Parent>
      <Child />
    </Parent>
  );
}

Đáp án:

// render ban đầu
Parent is rendered
Child is rendered

// useEffects
Child committed effect
Parent committed effect

Nếu bạn trả lời đúng — tuyệt vời! Nếu không, đừng lo — nhiều lập trình viên React cũng gặp khó khăn với vấn đề này. Thực tế, thông tin này không được giải thích rõ ràng trong tài liệu chính thức của React.

Hãy cùng tìm hiểu tại sao các component con được render sau cùng nhưng effect của chúng lại được commit đầu tiên. Chúng ta sẽ tìm hiểu cách React render component và commit các effect (useEffect). Chúng ta sẽ đề cập đến một số khái niệm nội bộ của React như kiến trúc React Fiberthuật toán duyệt của nó.

Tổng quan về React Internals

Theo tài liệu chính thức của React, toàn bộ vòng đời của component React có thể được chia thành 3 giai đoạn: Trigger → Render → Commit

Khởi tạo render

  • Render lần đầu tiên của component, hoặc cập nhật state với setState.
  • Cập nhật state được đưa vào hàng đợi và lên lịch xử lý bởi React Scheduler.

Giai đoạn render

  • React gọi component và xử lý cập nhật state.
  • React điều hòa (reconcile) và đánh dấu là "dirty" để chuẩn bị cho giai đoạn commit.
  • Tạo node DOM mới trong nội bộ.

Commit vào DOM

  • Áp dụng thao tác DOM thực tế.
  • Chạy các effect (useEffect, useLayoutEffect).

React Fiber Tree

Trước khi đi sâu vào thuật toán duyệt, chúng ta cần hiểu về kiến trúc React Fiber. Tôi sẽ cố gắng giữ phần giới thiệu này thân thiện với người mới.

Bên trong, React sử dụng cấu trúc dữ liệu dạng cây gọi là fiber tree để biểu diễn hệ thống phân cấp component và theo dõi các cập nhật.

React Fiber Tree structure

Từ sơ đồ trên, chúng ta có thể thấy rằng fiber tree không hoàn toàn ánh xạ 1:1 với DOM tree. Nó bao gồm thông tin bổ sung giúp React quản lý việc render hiệu quả hơn.

Mỗi node trong cây này được gọi là fiber node. Có nhiều loại fiber node khác nhau như HostComponent đề cập đến phần tử DOM gốc, như <div> hoặc <p> trong sơ đồ. FiberRootNode là node gốc và sẽ trỏ đến một node HostRoot khác trong mỗi lần render mới.

Mọi fiber node chứa các thuộc tính như props, state, và quan trọng nhất:

  1. child – Con của fiber.
  2. sibling – Anh/chị/em của fiber.
  3. return – Giá trị trả về của fiber là fiber cha.

Những thông tin này cho phép React hình thành một cây.

Mỗi khi có cập nhật state, React sẽ xây dựng một fiber tree mới và so sánh với cây cũ bên trong.

Cách fiber tree được duyệt

Nhìn chung, React sử dụng lại cùng một thuật toán duyệt trong nhiều trường hợp.

React traversal animation

Hình ảnh động trên cho thấy cách React duyệt fiber tree. Chú ý rằng mỗi node được duyệt hai lần. Quy tắc rất đơn giản:

  1. Duyệt xuống.
  2. Với mỗi fiber node, React kiểm tra:
    1. Nếu có con, di chuyển đến con.
    2. Nếu không có con, duyệt lại node hiện tại. Sau đó,
      1. Nếu có anh/chị/em, di chuyển đến anh/chị/em.
      2. Nếu không có anh/chị/em, di chuyển lên nút cha.

Thuật toán duyệt này đảm bảo mỗi node được duyệt hai lần.

Bây giờ, hãy xem lại câu đố ở trên.

Giai đoạn Render

React render phase animation

React duyệt fiber tree và thực hiện hai bước trên mỗi fiber node:

  • Trong bước đầu tiên, React gọi component — đây là nơi câu lệnh console.log được thực thi. React điều hòa và đánh dấu fiber là "dirty" nếu state hoặc props đã thay đổi, chuẩn bị cho giai đoạn commit.
  • Trong bước thứ hai, React xây dựng node DOM mới.

Vào cuối giai đoạn Render, một fiber tree mới với các node DOM đã cập nhật được tạo ra. Tại thời điểm này, không có gì được commit vào DOM thực tế. Các thay đổi DOM thực sự sẽ xảy ra trong giai đoạn Commit.

Giai đoạn Commit

Giai đoạn commit là nơi diễn ra thay đổi DOM thực tếthực thi các effect (useEffect). Mô hình duyệt vẫn giữ nguyên, nhưng các thay đổi DOM và thực thi effect được xử lý trong các lần duyệt riêng biệt.

Trong phần này, chúng ta sẽ bỏ qua các thay đổi DOM và tập trung vào phần thực thi effect.

Commit các effect

React sử dụng cùng một thuật toán duyệt. Tuy nhiên, thay vì kiểm tra xem một node có con hay không, nó kiểm tra xem node có cây con hay không — điều này có ý nghĩa, vì chỉ các component React mới có thể chứa hook useEffect. Một node DOM như <p> sẽ không chứa bất kỳ React hook nào.

Không có gì xảy ra trong bước đầu tiên, nhưng trong bước thứ hai, nó commit các effect.

React commit phase animation

Thuật toán duyệt theo chiều sâu này giải thích tại sao các effect của con được chạy trước các effect của cha. Đây là nguyên nhân gốc rễ.

Bây giờ, hãy xem xét một ví dụ câu đố khác. Kết quả bây giờ sẽ dễ hiểu hơn với bạn.

function Parent({ children }) {
  console.log("Parent is rendered");
  useEffect(() => {
    console.log("Parent committed effect");
  }, []);

  return <div>{children}</div>;
}

function Child() {
  console.log("Child is rendered");
  useEffect(() => {
    console.log("Child committed effect");
  }, []);

  return <p>Child</p>;
}

function ParentSibling() {
  console.log("ParentSibling is rendered");
  useEffect(() => {
    console.log("ParentSibling committed effect");
  }, []);

  return <p>Parent's Sibling</p>;
}

export default function App() {
  return (
    <>
      <Parent>
        <Child />
      </Parent>
      <ParentSibling />
    </>
  );
}

Đáp án:

// Render ban đầu
Parent is rendered
Child is rendered
ParentSibling is rendered

// useEffects
Child committed effect
Parent committed effect
ParentSibling committed effect

Trong giai đoạn commit:

Complex React commit phase animation

Bây giờ, bạn đã hiểu tại sao effect của các component con được thực thi trước effect của các component cha trong giai đoạn commit.

Hiểu cách React commit các hook useEffect có thể giúp bạn tránh được những lỗi tinh vi và hành vi không mong muốn—đặc biệt khi làm việc với cấu trúc component phức tạp.

Chào mừng bạn đến với thế giới nội tại của React!

Nguồn: frontendmasters

Nhận xét

Bài đăng phổ biến