Vòng Đời Component trong React: Từ Khởi Tạo Đến Kết Thúc (React Roadmap)

Chào mừng bạn quay trở lại với series “React Roadmap – Lộ trình học React 2025“! Sau khi đã cùng nhau khám phá React là gì, sự khác biệt giữa Class vs Functional Components, sức mạnh của JSX, cách quản lý dữ liệu với Props và State, và làm chủ Conditional Rendering cùng việc kết hợp các Component, hôm nay chúng ta sẽ đi sâu vào một khái niệm cốt lõi khác: **Vòng Đời (Lifecycle) của Component trong React**.

Nếu coi mỗi Component như một sinh vật sống trong ứng dụng của bạn, thì nó cũng có một “vòng đời” riêng: sinh ra (được thêm vào giao diện), lớn lên và thay đổi (cập nhật), và cuối cùng là “qua đời” (bị gỡ bỏ khỏi giao diện). Hiểu rõ vòng đời này là chìa khóa để bạn có thể thực hiện các tác vụ cần thiết vào đúng thời điểm, từ việc tải dữ liệu ban đầu cho đến dọn dẹp tài nguyên khi component không còn cần thiết nữa.

Bài viết này sẽ giúp bạn làm sáng tỏ các giai đoạn của vòng đời component trong React, khám phá cách các phương thức (Lifecycle Methods) trong Class Components và Hook `useEffect` trong Functional Components giúp chúng ta tương tác với vòng đời đó. Hãy cùng bắt đầu hành trình này nhé!

Vòng Đời Component trong React là gì?

Trong React, mỗi component có một chu kỳ sống độc lập, trải qua nhiều giai đoạn khác nhau từ lúc được tạo ra cho đến khi bị phá hủy. Vòng đời component về cơ bản là tập hợp các phương thức (đối với Class Components) hoặc các Hook (đối với Functional Components) mà React gọi tại các thời điểm cụ thể trong quá trình tồn tại của component. Việc nắm vững các điểm “giao thoa” này cho phép chúng ta kiểm soát và thực hiện các hành động phụ (side effects) như gọi API, đăng ký/hủy đăng ký sự kiện, thao tác trực tiếp với DOM, v.v., vào đúng lúc.

Tại sao cần hiểu về Vòng Đời Component?

Việc hiểu biết sâu sắc về vòng đời component mang lại nhiều lợi ích thiết thực:

  • Quản lý Side Effects: Hầu hết các tác vụ không liên quan trực tiếp đến việc hiển thị (rendering) giao diện – ví dụ: fetching data từ server, thiết lập timer, lắng nghe sự kiện DOM – đều cần được thực hiện trong các giai đoạn cụ thể của vòng đời để đảm bảo chúng hoạt động đúng và hiệu quả.
  • Tối ưu Hiệu năng: Một số phương thức trong vòng đời cho phép bạn kiểm soát việc component có nên re-render lại hay không, giúp tránh các cập nhật không cần thiết và cải thiện hiệu năng ứng dụng.
  • Dọn dẹp Tài nguyên: Khi một component bị gỡ bỏ, việc dọn dẹp các tài nguyên đã được tạo ra (như timer, listeners, subscriptions) là cực kỳ quan trọng để tránh rò rỉ bộ nhớ và các vấn đề không mong muốn khác. Vòng đời cung cấp điểm neo (hook) để thực hiện việc này.
  • Debugging Hiệu quả: Khi có lỗi hoặc hành vi bất thường xảy ra, việc hiểu rõ component đang ở giai đoạn nào trong vòng đời sẽ giúp bạn khoanh vùng và khắc phục vấn đề dễ dàng hơn.

Các Giai Đoạn Chính của Vòng Đời Component

Vòng đời của một component React có thể chia thành ba giai đoạn chính:

  1. Mounting (Khởi tạo): Giai đoạn component lần đầu tiên được tạo ra và chèn vào DOM (cây giao diện của trình duyệt). Đây là lúc component “chào đời”.
  2. Updating (Cập nhật): Giai đoạn component re-render lại do sự thay đổi của Props hoặc State. Component “lớn lên” và thay đổi diện mạo hoặc nội dung.
  3. Unmounting (Kết thúc): Giai đoạn component bị gỡ bỏ khỏi DOM. Component “qua đời” và biến mất khỏi giao diện.
  4. Error Handling (Xử lý Lỗi): Giai đoạn đặc biệt khi có lỗi xảy ra trong quá trình rendering, trong các phương thức vòng đời, hoặc trong constructor của bất kỳ component con nào.

Chúng ta sẽ lần lượt đi sâu vào từng giai đoạn này, xem cách Class Components và Functional Components (với Hooks) xử lý chúng.

Giai Đoạn 1: Mounting (Khởi Tạo)

Đây là giai đoạn component được “sinh ra” và lần đầu tiên xuất hiện trên giao diện. Thứ tự các bước trong giai đoạn này là quan trọng:

Đối với Class Components:

Các phương thức được gọi theo thứ tự:

  1. constructor(props): Đây là phương thức đầu tiên được gọi. Nó được sử dụng để:
    • Khởi tạo state local cho component bằng cách gán object vào this.state.
    • Binding các phương thức xử lý sự kiện (event handlers) vào instance của component.

    Lưu ý: Tránh gọi các side effects (như gọi API) hoặc thao tác với DOM trong constructor. Chỉ sử dụng nó cho việc khởi tạo state và binding methods.

    class MyComponent extends React.Component {
      constructor(props) {
        super(props); // Luôn gọi super(props) đầu tiên!
        this.state = { count: 0 }; // Khởi tạo state
        this.handleClick = this.handleClick.bind(this); // Binding method
        console.log('Constructor called');
      }
    
      handleClick() {
        // Xử lý sự kiện
      }
    
      render() {
        // ... giao diện
        return <div>Hello</div>;
      }
    
      componentDidMount() {
        // ... side effects
      }
    }
    
  2. static getDerivedStateFromProps(props, state): Đây là một phương thức hiếm khi được sử dụng, được gọi ngay trước mỗi lần render (cả mounting và updating). Nó tồn tại để đồng bộ state với props. Phương thức này phải trả về một object để cập nhật state, hoặc null nếu không có gì thay đổi. Nó không có quyền truy cập vào instance component (this).
  3. render(): Phương thức bắt buộc duy nhất trong class component. Nó tạo ra các element React (thường là JSX) mô tả giao diện mà bạn muốn hiển thị. Phương thức này không được gây ra side effects và phải luôn trả về cùng một kết quả cho cùng một props và state.
  4. componentDidMount(): Đây là phương thức được gọi ngay sau khi component và tất cả các component con của nó đã được render lần đầu tiên và được chèn vào DOM. Đây là nơi lý tưởng để thực hiện các side effects cần thiết sau khi component đã sẵn sàng trên trình duyệt:
    • Fetching data từ API.
    • Thiết lập subscriptions hoặc listeners.
    • Thao tác trực tiếp với DOM.
    • Khởi tạo các thư viện JavaScript không phải của React.
    class DataFetcher extends React.Component {
      constructor(props) {
        super(props);
        this.state = { data: null, loading: true };
      }
    
      componentDidMount() {
        console.log('Component DID mount, fetching data...');
        // Ví dụ: Gọi API để lấy dữ liệu sau khi component đã hiển thị
        fetch('https://api.example.com/data')
          .then(response => response.json())
          .then(data => this.setState({ data: data, loading: false }))
          .catch(error => console.error('Error fetching data:', error));
      }
    
      render() {
        if (this.state.loading) {
          return <div>Đang tải dữ liệu...</div>;
        }
        return <div>Dữ liệu: {JSON.stringify(this.state.data)}</div>;
      }
    }
    

Đối với Functional Components (với Hooks):

Với Functional Components và Hook useEffect, chúng ta xử lý các side effects liên quan đến mounting bằng cách:

  1. Sử dụng Hook useEffect.
  2. Truyền vào một hàm (chứa side effect của bạn) làm đối số đầu tiên.
  3. Truyền vào một mảng rỗng ([]) làm đối số thứ hai (dependency array). Mảng rỗng báo cho React biết rằng effect này chỉ nên chạy một lần sau lần render đầu tiên (tương tự componentDidMount).
import React, { useState, useEffect } from 'react';

function DataFetcherFunctional() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    console.log('Effect ran: fetching data (simulating componentDidMount)');
    // Tác vụ side effect: Fetch data
    fetch('https://api.example.com/data')
      .then(response => response.json())
      .then(data => {
        setData(data);
        setLoading(false);
      })
      .catch(error => console.error('Error fetching data:', error));

    // Cleanup function (tương tự componentWillUnmount), sẽ được gọi khi component unmount
    return () => {
      console.log('Cleanup function ran (simulating componentWillUnmount)');
      // Ví dụ: Hủy bỏ yêu cầu fetch nếu component unmount trước khi hoàn thành
      // (cần cơ chế hỗ trợ hủy bỏ, ví dụ: AbortController)
    };
  }, []); // Mảng rỗng: effect chỉ chạy một lần sau lần render đầu tiên

  if (loading) {
    return <div>Đang tải dữ liệu...</div>;
  }
  return <div>Dữ liệu: {JSON.stringify(data)}</div>;
}

Trong ví dụ trên, hàm bên trong useEffect với dependency array là [] sẽ chỉ chạy một lần sau khi component hiển thị lần đầu tiên, giống như componentDidMount. Hàm được trả về từ useEffect là hàm cleanup, sẽ được gọi khi component unmount (tương tự componentWillUnmount), chúng ta sẽ nói rõ hơn ở giai đoạn Unmounting.

Giai Đoạn 2: Updating (Cập Nhật)

Giai đoạn này xảy ra khi component re-render do sự thay đổi của Props hoặc State. Đây là giai đoạn lặp đi lặp lại trong vòng đời của component.

Đối với Class Components:

Các phương thức được gọi theo thứ tự:

  1. static getDerivedStateFromProps(props, state): (Đã giải thích ở trên) Được gọi trước mỗi lần render trong quá trình updating.
  2. shouldComponentUpdate(nextProps, nextState): Phương thức này cho phép bạn quyết định liệu React có nên tiếp tục quá trình re-render hay không. Mặc định, nó luôn trả về true. Bằng cách trả về false, bạn có thể ngăn chặn việc render lại không cần thiết, là một kỹ thuật tối ưu hiệu năng. Tuy nhiên, việc sử dụng nó đòi hỏi cẩn thận để tránh bỏ sót các cập nhật cần thiết. Thường thì bạn sẽ sử dụng React.PureComponent thay vì tự implement phương thức này.
  3. render(): (Đã giải thích ở trên) Tạo ra các element React mới dựa trên props và state hiện tại.
  4. getSnapshotBeforeUpdate(prevProps, prevState): Được gọi ngay trước khi các thay đổi từ render mới được “commit” (áp dụng) vào DOM thực. Phương thức này cho phép bạn lấy thông tin về DOM (ví dụ: vị trí cuộn) trước khi nó bị thay đổi. Giá trị trả về của phương thức này sẽ được truyền làm đối số thứ ba cho componentDidUpdate.
  5. componentDidUpdate(prevProps, prevState, snapshot): Được gọi ngay sau khi việc cập nhật (rendering) hoàn tất và DOM đã được cập nhật. Đây là nơi lý tưởng để:
    • Thực hiện các side effects dựa trên sự thay đổi của props hoặc state (ví dụ: gọi API mới khi một prop thay đổi).
    • Cập nhật DOM dựa trên dữ liệu mới (nhưng thường thì React đã xử lý việc này cho bạn).
    • So sánh prevPropsprevState với this.propsthis.state để thực hiện các hành động có điều kiện.

    Lưu ý: Cần cẩn thận khi gọi setState trong componentDidUpdate để tránh tạo ra vòng lặp cập nhật vô hạn. Luôn đặt điều kiện kiểm tra sự thay đổi trước khi gọi setState.

    class UserProfile extends React.Component {
      componentDidUpdate(prevProps) {
        console.log('Component DID update');
        // Ví dụ: Fetch dữ liệu mới nếu ID người dùng trong props thay đổi
        if (this.props.userId !== prevProps.userId) {
          console.log('User ID changed, fetching new profile...');
          this.fetchUserData(this.props.userId);
        }
      }
    
      fetchUserData(userId) {
        // Logic gọi API
      }
    
      render() {
        // ... giao diện hiển thị thông tin user
        return <div>Profile for User ID: {this.props.userId}</div>;
      }
    }
    

Đối với Functional Components (với Hooks):

Với useEffect, chúng ta xử lý các side effects liên quan đến updating bằng cách:

  1. Sử dụng Hook useEffect.
  2. Truyền vào một hàm (chứa side effect của bạn) làm đối số đầu tiên.
  3. Truyền vào một mảng các dependencies (các biến state hoặc props mà effect phụ thuộc vào) làm đối số thứ hai. Hook sẽ chỉ chạy lại khi *một trong* các dependencies này thay đổi giữa các lần render.
import React, { useState, useEffect } from 'react';

function UserProfileFunctional({ userId }) {
  const [userData, setUserData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    console.log('Effect ran: fetching user data (simulating componentDidUpdate)');
    setLoading(true);
    // Tác vụ side effect: Fetch dữ liệu user khi userId thay đổi
    fetch(`https://api.example.com/users/${userId}`)
      .then(response => response.json())
      .then(data => {
        setUserData(data);
        setLoading(false);
      })
      .catch(error => console.error('Error fetching user:', error));

    // Cleanup function (nếu cần hủy bỏ request hoặc dọn dẹp các listener cũ)
    return () => {
      console.log('Cleanup before running new effect or unmounting');
      // Ví dụ: Hủy bỏ yêu cầu fetch trước đó nếu một yêu cầu mới được kích hoạt
      // (cần cơ chế hỗ trợ hủy bỏ)
    };

  }, [userId]); // Dependency array: effect chạy lại khi userId thay đổi

  if (loading) {
    return <div>Đang tải profile...</div>;
  }
  return <div>Thông tin User: {JSON.stringify(userData)}</div>;
}

Trong ví dụ này, effect sẽ chạy lần đầu tiên (mounting) và sau đó mỗi khi giá trị của userId trong dependency array thay đổi. Hàm cleanup sẽ chạy trước khi effect mới chạy (do userId thay đổi) và khi component unmount. Điều này giúp đồng bộ hóa side effect với các props hoặc state liên quan.

Giai Đoạn 3: Unmounting (Kết Thúc)

Đây là giai đoạn cuối cùng, khi component bị gỡ bỏ khỏi DOM. Tại thời điểm này, bạn cần dọn dẹp mọi tài nguyên đã được thiết lập trong các giai đoạn trước để tránh rò rỉ bộ nhớ và các vấn đề khác.

Đối với Class Components:

  1. componentWillUnmount(): Phương thức này được gọi ngay trước khi component bị hủy và gỡ bỏ khỏi DOM. Đây là nơi lý tưởng để:
    • Hủy bỏ các timer đã thiết lập bằng setTimeout hoặc setInterval.
    • Hủy đăng ký các sự kiện DOM hoặc subscriptions.
    • Hủy bỏ bất kỳ tài nguyên nào được tạo ra trong componentDidMount hoặc componentDidUpdate.
    class TimerComponent extends React.Component {
      constructor(props) {
        super(props);
        this.state = { seconds: 0 };
        this.timer = null;
      }
    
      componentDidMount() {
        console.log('Timer component mounted');
        this.timer = setInterval(() => {
          this.setState(prevState => ({
            seconds: prevState.seconds + 1
          }));
        }, 1000);
      }
    
      componentWillUnmount() {
        console.log('Timer component WILL unmount, clearing timer...');
        // Dọn dẹp timer khi component bị gỡ bỏ
        if (this.timer) {
          clearInterval(this.timer);
          console.log('Timer cleared');
        }
      }
    
      render() {
        return <div>Thời gian trôi qua: {this.state.seconds} giây</div>;
      }
    }
    

Đối với Functional Components (với Hooks):

Với useEffect, việc dọn dẹp được thực hiện thông qua hàm được trả về từ effect function:

  1. Nếu effect function của bạn trả về một hàm, hàm đó sẽ được chạy trước khi effect tiếp theo chạy (trong giai đoạn updating) và khi component unmount.
  2. Hàm được trả về này chính là hàm cleanup, nơi bạn đặt logic dọn dẹp của mình.
import React, { useState, useEffect } from 'react';

function TimerComponentFunctional() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    console.log('Timer effect ran: setting up interval');
    // Thiết lập interval
    const intervalId = setInterval(() => {
      setSeconds(prevSeconds => prevSeconds + 1);
    }, 1000);

    // Hàm cleanup: được gọi khi component unmount hoặc khi dependencies thay đổi
    return () => {
      console.log('Timer cleanup function ran: clearing interval');
      clearInterval(intervalId);
    };
  }, []); // Mảng rỗng: effect chỉ chạy một lần sau mount, cleanup chỉ chạy khi unmount

  return <div>Thời gian trôi qua: {seconds} giây</div>;
}

Trong ví dụ này, setInterval được thiết lập khi component mount. Hàm được trả về từ useEffect sẽ gọi clearInterval khi component unmount, đảm bảo timer được tắt đúng cách và không gây rò rỉ bộ nhớ.

Giai Đoạn 4: Error Handling (Xử lý Lỗi)

Giai đoạn này xảy ra khi có lỗi JavaScript xảy ra trong quá trình rendering, trong các phương thức vòng đời, hoặc trong constructor của bất kỳ component con nào trong cây. Để xử lý lỗi trong giai đoạn này, bạn cần sử dụng Error Boundaries.

Đối với Class Components:

  1. static getDerivedStateFromError(error): Được gọi sau khi có lỗi xảy ra trong bất kỳ component con nào. Nó nhận lỗi làm đối số và nên trả về một object để cập nhật state, cho phép bạn render một giao diện dự phòng (fallback UI).
  2. componentDidCatch(error, errorInfo): Được gọi sau khi có lỗi xảy ra trong bất kỳ component con nào. Bạn có thể sử dụng nó để ghi log lỗi.
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // Cập nhật state để lần render tiếp theo sẽ hiển thị fallback UI
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // Bạn cũng có thể ghi log lỗi vào một dịch vụ báo cáo lỗi
    console.error("Uncaught error:", error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // Bạn có thể render bất kỳ fallback UI tùy chỉnh nào
      return <h1>Đã có lỗi xảy ra.</h1>;
    }

    return this.props.children;
  }
}

// Sử dụng Error Boundary
// <ErrorBoundary>
//   <MyProblematicComponent />
// </ErrorBoundary>

Đối với Functional Components (với Hooks):

Hooks hiện tại không có cách trực tiếp để implement Error Boundaries *trong* Functional Components. Bạn vẫn cần sử dụng Class Components để tạo Error Boundaries và bọc các Functional Components có khả năng gây lỗi bên trong chúng.

Tóm Lược: Class Lifecycle Methods vs. `useEffect` Hook

Hooks, đặc biệt là useEffect, được thiết kế để giải quyết các nhu cầu tương tự như các phương thức vòng đời trong Class Components, nhưng với một cách tiếp cận tập trung vào “effect” thay vì “thời điểm”. Dưới đây là bảng tóm tắt sự tương quan (không hoàn toàn 1-1):

Giai Đoạn Class Component Methods Functional Component (với Hook useEffect) Mục Đích Chính
Mounting
(Khi component lần đầu xuất hiện)
constructor()
static getDerivedStateFromProps()
render()
componentDidMount()
Function body
useEffect(() => { ... }, [])
(Effect với mảng rỗng)
Khởi tạo state & binding methods (constructor); Tạo JSX (render);
Thực hiện side effects lần đầu (fetch data, setup listeners) (componentDidMount / useEffect)
Updating
(Khi props hoặc state thay đổi)
static getDerivedStateFromProps()
shouldComponentUpdate()
render()
getSnapshotBeforeUpdate()
componentDidUpdate()
Function body
useEffect(() => { ... }, [dependencies])
(Effect với dependency array không rỗng)
Quyết định re-render (shouldComponentUpdate); Tạo JSX mới (render);
Thực hiện side effects khi dependencies thay đổi (fetch data lại, update DOM) (componentDidUpdate / useEffect)
Unmounting
(Khi component bị gỡ bỏ)
componentWillUnmount() Hàm được trả về từ useEffect
useEffect(() => { return () => { ... cleanup ... }; }, [...])
Dọn dẹp tài nguyên (clear timers, remove listeners, cancel subscriptions) (componentWillUnmount / cleanup function trong useEffect)
Error Handling
(Khi có lỗi trong cây con)
static getDerivedStateFromError()
componentDidCatch()
(Không có Hook tương đương trực tiếp)
Cần sử dụng Class Component làm Error Boundary
Hiển thị giao diện dự phòng; Ghi log lỗi

Hooks useEffect là cách hiện đại và được khuyến khích để quản lý side effects trong Functional Components. Nó linh hoạt hơn các phương thức vòng đời truyền thống vì cùng một Hook có thể xử lý logic cho nhiều giai đoạn khác nhau (mount, update, unmount) tùy thuộc vào dependency array và hàm cleanup.

Lời Khuyên Khi Làm Việc Với Vòng Đời và useEffect

  • Sử dụng useEffect đúng cách: Luôn cẩn thận với dependency array của useEffect. Nếu bỏ sót dependency, effect của bạn có thể không chạy lại khi cần, dẫn đến lỗi logic. Nếu dependency array rỗng ([]), effect chạy một lần sau mount và cleanup khi unmount. Nếu không có dependency array, effect chạy sau *mỗi* lần render (thường không mong muốn).
  • Cleanup là quan trọng: Nếu effect của bạn thiết lập một tài nguyên (timer, subscription, listener), hãy đảm bảo bạn có hàm cleanup để dọn dẹp nó, đặc biệt là khi component unmount hoặc khi effect chạy lại do dependencies thay đổi.
  • Tránh side effects trong render: Tuyệt đối không thực hiện các tác vụ như gọi API, sửa đổi state (trừ khi là khởi tạo state lazy với useState), hoặc thao tác trực tiếp với DOM trong hàm render (đối với class) hoặc body của functional component (trước khi gọi các Hook). Điều này sẽ gây ra các vấn đề hiệu năng và hành vi không thể đoán trước.
  • Sử dụng Error Boundaries: Luôn bọc các phần của ứng dụng có khả năng gây lỗi trong Error Boundaries để ngăn chặn toàn bộ ứng dụng bị crash khi có lỗi xảy ra ở một component con.
  • Hiểu sự khác biệt giữa Class và Functional: Mặc dù Hooks cung cấp khả năng tương tự, cách tiếp cận và mô hình mental model có sự khác biệt. Hãy hiểu rõ cách mỗi cách hoạt động để chọn phương pháp phù hợp (mặc dù Functional Components với Hooks đang là xu hướng chủ đạo).

Kết Luận

Hiểu rõ vòng đời của component là một bước tiến quan trọng trên hành trình làm chủ React của bạn. Nó cung cấp cho bạn quyền kiểm soát mạnh mẽ đối với các side effects và logic không trực tiếp liên quan đến việc hiển thị giao diện. Dù bạn đang làm việc với Class Components và các phương thức vòng đời cổ điển, hay sử dụng Functional Components hiện đại với Hook useEffect, các nguyên tắc cơ bản về các giai đoạn tồn tại của component vẫn không thay đổi: Mount, Update, Unmount.

Nắm vững kiến thức này, bạn sẽ có thể viết code React hiệu quả hơn, ít lỗi hơn và dễ dàng debug khi có sự cố. Tiếp theo trên React Roadmap, chúng ta sẽ đi sâu hơn vào cách quản lý state phức tạp hơn và chia sẻ logic giữa các component bằng cách tìm hiểu về **React Context và Redux**. Hãy cùng chờ đón nhé!

Chúc bạn học tốt và hẹn gặp lại trong bài viết tiếp theo!

Chỉ mục