Class vs Functional Components: Bước Chuyển Lớn Trong Thế Giới React Hiện Đại

Giới Thiệu: Nền Tảng Components Của React

Chào mừng quay trở lại với series “Lộ Trình React” của chúng ta! Sau khi đã tìm hiểu “React là gì và vì sao nó lại phổ biến đến vậy“, đã đến lúc chúng ta đi sâu vào trái tim của React: Components.

Components là những khối xây dựng cơ bản giúp chúng ta tạo ra giao diện người dùng (UI) độc lập, có thể tái sử dụng và quản lý được. Về cơ bản, mọi thứ bạn thấy trên màn hình trong một ứng dụng React đều được tạo nên từ các components lồng ghép vào nhau. Việc hiểu rõ cách viết và quản lý components là yếu tố then chốt để trở thành một nhà phát triển React giỏi.

Trong lịch sử phát triển của React, đã có hai cách chính để định nghĩa components: Class ComponentsFunctional Components. Ban đầu, Class Components là lựa chọn mặc định khi bạn cần quản lý state nội bộ hoặc xử lý các tác vụ phức tạp liên quan đến vòng đời của component (lifecycle methods). Functional Components lúc đó chỉ được xem như những “component trình bày” đơn giản, không có state hay vòng đời riêng.

Tuy nhiên, với sự ra đời của React Hooks vào phiên bản 16.8, bức tranh đã thay đổi đáng kể. Hooks đã mang sức mạnh của state và lifecycle methods vào Functional Components, tạo nên một sự dịch chuyển lớn trong cộng đồng React. Ngày nay, Functional Components kết hợp với Hooks đã trở thành cách tiếp cận phổ biến và được khuyến khích nhất để xây dựng ứng dụng React hiện đại.

Bài viết này sẽ khám phá sâu hơn về Class Components và Functional Components, phân tích những điểm mạnh, điểm yếu của từng loại trong bối cảnh lịch sử và hiện tại, lý giải vì sao Hooks lại là “kẻ thay đổi cuộc chơi” và tại sao Functional Components với Hooks lại trở thành tiêu chuẩn mới.

Class Components: Cách Truyền Thống và Những Thách Thức

Trước khi Hooks xuất hiện, khi bạn cần một component có khả năng “ghi nhớ” dữ liệu (state) hoặc thực hiện các hành động tại các thời điểm cụ thể trong quá trình tồn tại của nó trên giao diện (lifecycle), bạn bắt buộc phải sử dụng Class Component.

Một Class Component được định nghĩa bằng cách tạo ra một lớp (class) JavaScript mở rộng từ React.Component. Nó yêu cầu có một phương thức render() trả về JSX để định nghĩa giao diện người dùng.

Cấu trúc cơ bản của một Class Component

Dưới đây là một ví dụ đơn giản về Class Component, một bộ đếm cơ bản:

import React from 'react';

class CounterClass extends React.Component {
  // Constructor được gọi đầu tiên khi component được tạo
  constructor(props) {
    super(props); // Luôn gọi super(props)
    // Khởi tạo state nội bộ của component
    this.state = {
      count: 0
    };
    // Binding các phương thức xử lý sự kiện để đảm bảo 'this' trỏ đúng đến component instance
    this.handleIncrement = this.handleIncrement.bind(this);
    this.handleDecrement = this.handleDecrement.bind(this);
  }

  // Phương thức vòng đời: được gọi sau khi component lần đầu tiên được render vào DOM
  componentDidMount() {
    console.log('CounterClass component đã được mount!');
    // Thích hợp để fetch data, thiết lập subscriptions, tương tác với DOM
  }

  // Phương thức vòng đời: được gọi sau khi component được cập nhật (state hoặc props thay đổi)
  componentDidUpdate(prevProps, prevState) {
    console.log('CounterClass component đã được cập nhật!');
    // Thực hiện hành động dựa trên sự thay đổi của state hoặc props
    if (prevState.count !== this.state.count) {
       console.log(`Count đã thay đổi từ ${prevState.count} sang ${this.state.count}`);
    }
  }

  // Phương thức vòng đời: được gọi ngay trước khi component bị gỡ bỏ khỏi DOM
  componentWillUnmount() {
    console.log('CounterClass component sẽ unmount!');
    // Thực hiện cleanup: hủy subscriptions, xóa timers, v.v.
  }

  // Phương thức xử lý sự kiện để tăng giá trị count
  handleIncrement() {
    // Cập nhật state. setState là bất đồng bộ.
    this.setState({ count: this.state.count + 1 });
  }

  // Phương thức xử lý sự kiện để giảm giá trị count
  handleDecrement() {
     // Cách cập nhật state dựa trên giá trị trước đó
    this.setState((prevState) => ({
        count: prevState.count - 1
    }));
  }


  // Phương thức render là bắt buộc, định nghĩa những gì component hiển thị
  render() {
    // Truy cập state thông qua this.state
    const { count } = this.state;
    // Truy cập props thông qua this.props
    // const { initialValue } = this.props; // Example if props were passed

    return (
      <div>
        <h3>Class Component Counter</h3>
        <p>Giá trị hiện tại: <strong>{count}</strong></p>
        <button onClick={this.handleIncrement}>Tăng</button>
         &nbsp;
        <button onClick={this.handleDecrement}>Giảm</button>
      </div>
    );
  }
}

export default CounterClass;

Đặc điểm chính của Class Components:

  • State: Quản lý state nội bộ thông qua this.state và cập nhật bằng this.setState().
  • Props: Truy cập props thông qua this.props.
  • Lifecycle Methods: Cung cấp một bộ các phương thức được gọi tại các giai đoạn khác nhau trong vòng đời của component (mounting, updating, unmounting). Đây là nơi bạn xử lý các side effects như fetch data, thiết lập subscriptions, v.v.
  • this: Phụ thuộc nhiều vào từ khóa this, có thể gây nhầm lẫn và đòi hỏi phải binding các phương thức xử lý sự kiện đúng cách.

Những thách thức với Class Components:

Mặc dù mạnh mẽ, Class Components bộc lộ một số hạn chế khi ứng dụng trở nên phức tạp:

  • Sự phức tạp của this: Việc hiểu và quản lý từ khóa this trong JavaScript, đặc biệt là trong các event handler, thường là một điểm khó khăn cho nhiều lập trình viên, đặc biệt là người mới. Việc quên bind có thể dẫn đến lỗi runtime khó chịu.
  • Boilerplate nhiều hơn: So với Functional Components, Class Components thường yêu cầu nhiều code “khuôn mẫu” (boilerplate) hơn chỉ để thiết lập một component cơ bản có state hoặc lifecycle. Bạn cần constructor, super(props), và định nghĩa phương thức render rõ ràng.
  • Khó tái sử dụng logic stateful: Việc chia sẻ logic phức tạp giữa các components (ví dụ: logic fetch data với trạng thái loading/error, logic subscription) thường không dễ dàng. Các pattern như Higher-Order Components (HOCs) hoặc Render Props được sinh ra để giải quyết vấn đề này, nhưng chúng có thể làm tăng độ phức tạp của cây component (render prop drilling, wrapper hell).
  • Logic liên quan bị phân tán: Logic cho một tính năng duy nhất (ví dụ: thiết lập và dọn dẹp một subscription) thường bị phân tán qua nhiều phương thức vòng đời khác nhau (componentDidMount để thiết lập, componentWillUnmount để dọn dẹp, componentDidUpdate để xử lý thay đổi). Điều này làm cho code khó đọc, khó hiểu và khó bảo trì khi bạn cần theo dõi luồng xử lý của một tính năng cụ thể.

Functional Components (Trước và Sau Khi Có Hooks)

Ban đầu, Functional Components (hay còn gọi là “Stateless Functional Components” – SFCs) rất đơn giản. Chúng chỉ là các hàm JavaScript nhận vào props như một đối số duy nhất và trả về JSX.

Functional Components (Trước Hooks): Chỉ Dùng Để Trình Bày

Chúng được sử dụng chủ yếu cho các components “trình bày” (presentational components) chỉ hiển thị giao diện dựa trên props nhận được và không cần quản lý state nội bộ hay thực hiện các side effects phức tạp.

import React from 'react';

// Functional Component đơn giản nhận props và trả về JSX
function Greeting(props) {
  return (
    <div>
      <p>Xin chào, <strong>{props.name}</strong>!</p>
    </div>
  );
}

export default Greeting;

// Hoặc sử dụng cú pháp arrow function (phổ biến hơn hiện nay)
// const Greeting = (props) => {
//   return (
//     <div>
//       <p>Xin chào, <strong>{props.name}</strong>!</p>
//     </div>
//   );
// };

Ưu điểm của chúng là đơn giản, dễ đọc và viết. Nhược điểm là không thể làm được những việc cần state hoặc lifecycle.

Bước Ngoặt Lớn: React Hooks

Mọi thứ thay đổi vào tháng 2 năm 2019 khi React 16.8 ra mắt Hooks. Hooks là các hàm cho phép bạn “móc nối” (hook into) các tính năng của React (như state và lifecycle methods) từ các Functional Components. Với Hooks, Functional Components không còn là “stateless” nữa; chúng có thể quản lý state, thực hiện side effects và làm được mọi thứ mà trước đây chỉ có Class Components làm được.

Mục tiêu chính của việc giới thiệu Hooks là để giải quyết những hạn chế đã đề cập của Class Components, đặc biệt là về khả năng tái sử dụng logic stateful và sự phức tạp của this/lifecycle methods.

Functional Components (với Hooks): Tiêu Chuẩn Hiện Đại

Với Hooks, Functional Components trở nên cực kỳ mạnh mẽ và linh hoạt. Chúng vẫn giữ được sự đơn giản của các hàm JavaScript, nhưng giờ đây có thể có state và side effects.

Sử dụng State với useState

Hook useState cho phép bạn thêm state vào Functional Components. Nó nhận giá trị khởi tạo làm đối số và trả về một mảng chứa hai phần tử: biến state hiện tại và một hàm để cập nhật nó.

import React, { useState } from 'react';

function CounterFunctional() {
  // Khai báo một biến state 'count' và hàm cập nhật 'setCount', giá trị khởi tạo là 0
  const [count, setCount] = useState(0);
  // Bạn có thể khai báo nhiều state độc lập
  // const [name, setName] = useState('Guest');

  const handleIncrement = () => {
    // Sử dụng hàm setCount để cập nhật state
    setCount(count + 1);
     // Nếu cập nhật dựa trên giá trị trước đó, nên dùng functional update:
     // setCount(prevCount => prevCount + 1);
  };

  const handleDecrement = () => {
       setCount(prevCount => prevCount - 1);
  }


  return (
    <div>
      <h3>Functional Component Counter (with useState)</h3>
      <p>Giá trị hiện tại: <strong>{count}</strong></p>
      <button onClick={handleIncrement}>Tăng</button>
      &nbsp;
      <button onClick={handleDecrement}>Giảm</button>
    </div>
  );
}

export default CounterFunctional;

So sánh với Class Component Counter, phiên bản Functional với useState trông gọn gàng và trực quan hơn đáng kể.

Thực hiện Side Effects với useEffect

Hook useEffect cho phép bạn thực hiện các side effects trong Functional Components. Side effects là những hành động xảy ra bên ngoài luồng dữ liệu chính, ví dụ: fetch data, thao tác trực tiếp với DOM, thiết lập subscriptions, v.v.

useEffect nhận một hàm làm đối số đầu tiên (gọi là “effect function”). Hàm này sẽ chạy sau khi component được render. Nó có thể trả về một hàm “cleanup”, sẽ chạy trước khi effect chạy lại (do state/props thay đổi) hoặc khi component bị unmount.

Đối số thứ hai của useEffect là một mảng dependencies (tùy chọn). Mảng này cho React biết khi nào nên chạy lại effect. Nếu mảng rỗng ([]), effect chỉ chạy một lần sau lần render đầu tiên (tương tự componentDidMount) và hàm cleanup chỉ chạy khi component unmount (tương tự componentWillUnmount). Nếu mảng chứa các biến, effect sẽ chạy lại bất cứ khi nào các biến đó thay đổi (tương tự componentDidUpdate). Nếu không có đối số thứ hai, effect sẽ chạy sau mỗi lần render.

import React, { useState, useEffect } from 'react';

function DataLoader({ userId }) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  // useEffect để fetch data khi component mount hoặc userId thay đổi
  useEffect(() => {
    console.log(`Fetching data for user ID: ${userId}`);
    setLoading(true);
    setError(null); // Reset error state on new fetch

    const fetchData = async () => {
      try {
        // Giả lập việc fetch data từ API
        const response = await fetch(`https://api.example.com/users/${userId}`); // Thay thế bằng API thật
        if (!response.ok) {
          throw new Error(`Lỗi HTTP! Status: ${response.status}`);
        }
        const result = await response.json();
        setData(result);
      } catch (err) {
        setError(err);
      } finally {
        setLoading(false);
      }
    };

    fetchData();

    // Hàm cleanup: chạy khi component unmount hoặc effect chạy lại
    return () => {
      console.log(`Cleaning up effect for user ID: ${userId}`);
      // Đây là nơi bạn hủy các subscription, clear timer, hoặc hủy các request đang chạy dở
      // Ví dụ: abortController.abort(); nếu dùng Fetch API với AbortController
    };

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

  if (loading) return <p>Đang tải dữ liệu...</p>;
  if (error) return <p>Lỗi khi tải dữ liệu: {error.message}</p>;

  return (
    <div>
      <h3>Thông tin người dùng (Functional Component với useEffect):</h3>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
}

export default DataLoader;

useEffect cung cấp một cách mạnh mẽ và linh hoạt để quản lý side effects, thay thế gần hết các trường hợp sử dụng của componentDidMount, componentDidUpdate, và componentWillUnmount.

Tái Sử Dụng Logic với Custom Hooks

Một trong những lợi ích đột phá nhất của Hooks là khả năng tạo ra Custom Hooks. Custom Hooks là các hàm JavaScript thông thường có tên bắt đầu bằng use và gọi các Hooks khác bên trong chúng. Chúng cho phép bạn đóng gói logic stateful và side-effectful và chia sẻ nó một cách dễ dàng giữa các components khác nhau mà không cần thay đổi cấu trúc cây component.

Ví dụ: tạo một hook để theo dõi trạng thái online/offline của trình duyệt:

import { useState, useEffect } from 'react';

function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(navigator.onLine);

  useEffect(() => {
    const handleOnline = () => setIsOnline(true);
    const handleOffline = () => setIsOnline(false);

    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);

    // Cleanup: xóa event listeners khi component unmount
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []); // Empty dependency array: effect chỉ chạy một lần khi mount/unmount

  return isOnline;
}

// Cách sử dụng custom hook này trong một component
function StatusIndicator() {
  const isOnline = useOnlineStatus();

  return (
    <p>Trạng thái kết nối: <strong style={{ color: isOnline ? 'green' : 'red' }}>
      {isOnline ? 'Online' : 'Offline'}
    </strong></p>
  );
}

export default StatusIndicator;

Custom Hooks giúp bạn viết code React một cách sạch sẽ, có tổ chức và dễ tái sử dụng hơn rất nhiều.

Tại Sao Có Sự Chuyển Mình Này? Ưu Điểm Vượt Trội của Functional Components với Hooks

Sự chuyển dịch mạnh mẽ sang Functional Components với Hooks không phải là một trào lưu nhất thời mà dựa trên những lợi ích cốt lõi mà cách tiếp cận này mang lại:

  • Đơn giản và dễ đọc hơn: Code Functional Component thường ngắn gọn hơn, ít boilerplate hơn Class Component. Cú pháp hàm giúp code trông giống JavaScript thuần túy hơn, dễ tiếp cận hơn cho người mới.
  • Quản lý state và side effects trực quan hơn: Hooks như useStateuseEffect cho phép bạn đưa logic stateful và side-effectful vào ngay bên trong Functional Component. Thay vì phân tán logic qua các phương thức vòng đời, bạn có thể nhóm logic liên quan lại với nhau trong cùng một effect (ví dụ: thiết lập và dọn dẹp subscription trong cùng một useEffect).
  • Khả năng tái sử dụng logic vượt trội: Custom Hooks là một cách cực kỳ hiệu quả để đóng gói và chia sẻ logic giữa các components mà không cần thay đổi cấu trúc component tree (như HOCs hoặc Render Props có thể gây ra). Điều này dẫn đến code sạch hơn và dễ bảo trì hơn.
  • Không cần bận tâm về this: Functional Components không có this riêng của chúng, loại bỏ hoàn toàn sự phức tạp và những lỗi tiềm ẩn liên quan đến việc binding this trong Class Components.
  • Dễ kiểm thử hơn: Functional Components, đặc biệt khi sử dụng Hooks và Custom Hooks, thường dễ viết unit test hơn vì chúng có xu hướng đóng gói logic thành các hàm có thể test độc lập.
  • Chuẩn bị cho tương lai của React: Mặc dù Class Components vẫn được hỗ trợ, tất cả các tính năng mới và các cải tiến hiệu năng trong React (như Concurrent Mode) được thiết kế để hoạt động tốt nhất với Hooks. Việc sử dụng Hooks giúp ứng dụng của bạn sẵn sàng hơn cho những tiến bộ trong tương lai của React.

Class Components: Vẫn Còn Chỗ Đứng?

Với tất cả những ưu điểm của Functional Components với Hooks, liệu Class Components còn chỗ đứng trong React hiện đại?

Câu trả lời là có, nhưng chủ yếu là trong các ngữ cảnh sau:

  • Codebase cũ: Hàng triệu dòng code React hiện tại được viết bằng Class Components. Bạn chắc chắn sẽ gặp và làm việc với chúng khi tham gia các dự án đã tồn tại lâu đời. Việc hiểu cách Class Components hoạt động là rất quan trọng để bảo trì và mở rộng các codebase này.
  • Error Boundaries: Hiện tại, Error Boundaries (cơ chế bắt lỗi trong cây component) chỉ có thể được triển khai dưới dạng Class Components sử dụng phương thức vòng đời componentDidCatch hoặc static getDerivedStateFromError. Đây là một trường hợp ngoại lệ hiếm hoi mà bạn vẫn cần Class Component.

React team đã khẳng định rằng họ không có kế hoạch loại bỏ Class Components trong tương lai gần để đảm bảo khả năng tương thích ngược. Tuy nhiên, khuyến nghị rõ ràng cho việc phát triển mới là sử dụng Functional Components với Hooks.

So Sánh: Class vs Functional Components

Để tóm tắt, dưới đây là bảng so sánh các đặc điểm chính của ba giai đoạn:

Đặc điểm Class Component Functional Component (trước Hooks) Functional Component (với Hooks)
Định nghĩa Mở rộng từ React.Component Hàm JavaScript thông thường Hàm JavaScript thông thường
State nội bộ Có (this.state, this.setState) Không Có (với useState)
Phương thức vòng đời / Side Effects Có (componentDidMount, componentDidUpdate, componentWillUnmount, v.v.) Không Có (với useEffect)
Quản lý ‘this’ Cần bind trong các event handler Không có ‘this’ Không có ‘this’
Boilerplate Thường có nhiều boilerplate hơn (constructor, super, render, methods binding) Rất ít, chỉ cần hàm trả về JSX Ít hơn Class Component, vẫn giữ được sự đơn giản của hàm
Khả năng tái sử dụng logic stateful Khó khăn hơn, thường dùng HOCs/Render Props có thể làm phức tạp cây component Không thể Dễ dàng và hiệu quả hơn với Custom Hooks
Cách truy cập Props this.props Đối số của hàm Đối số của hàm
Cách tiếp cận được khuyến khích trong React hiện đại Không phải là lựa chọn đầu tiên cho code mới Chỉ dùng cho components không cần state/effects (rất ít dùng hiện nay) Là tiêu chuẩn mới, được ưu tiên cho phát triển mới

Di Chuyển Từ Class Sang Functional Components

Nếu bạn đang làm việc với một codebase cũ sử dụng Class Components và muốn hiện đại hóa nó, việc chuyển đổi sang Functional Components với Hooks là một quá trình phổ biến, thường được gọi là “refactoring”.

Quá trình này thường bao gồm các bước như:

  1. Chuyển cấu trúc class thành một hàm.
  2. Thay thế this.statethis.setState bằng useState.
  3. Ánh xạ logic từ các phương thức vòng đời (componentDidMount, componentDidUpdate, componentWillUnmount) sang một hoặc nhiều hook useEffect với dependency array phù hợp và hàm cleanup.
  4. Thay thế việc sử dụng this.props bằng đối số props của hàm.
  5. Đóng gói các logic phức tạp, có thể tái sử dụng thành Custom Hooks riêng.

Việc refactor này không chỉ giúp code của bạn tuân thủ các best practice hiện đại mà còn thường làm cho code dễ đọc, dễ hiểu và dễ bảo trì hơn đáng kể. Tuy nhiên, đây có thể là một công việc tốn thời gian đối với các components phức tạp và cần được thực hiện cẩn thận với kiểm thử đầy đủ.

Kết Luận: Nắm Bắt Tương Lai Với Functional Components

Sự ra đời của React Hooks và sự dịch chuyển mạnh mẽ sang Functional Components đánh dấu một bước tiến quan trọng và tích cực trong cách chúng ta xây dựng ứng dụng với React. Chúng mang lại sự đơn giản, khả năng tái sử dụng logic mạnh mẽ và trải nghiệm phát triển tốt hơn cho đa số các trường hợp.

Đối với các bạn đang học và đi theo “Lộ Trình React” của mình, việc nắm vững cách làm việc với Functional Components và Hooks là điều cực kỳ quan trọng. Đây là cách React hiện đại hoạt động, và là kỹ năng cần thiết để xây dựng các ứng dụng hiệu quả, dễ bảo trì.

Hãy thực hành viết các components mới dưới dạng functional components với Hooks, thử nghiệm với các Hooks khác nhau và khám phá sức mạnh của Custom Hooks. Đừng ngại ngần khi gặp các codebase cũ dùng Class Components, vì hiểu biết về chúng vẫn là một phần của lịch sử React và cần thiết cho việc bảo trì, nhưng hãy tập trung vào cách tiếp cận hiện đại cho công việc mới.

Trong bài viết tiếp theo của series, chúng ta sẽ đi sâu hơn vào hai hook cơ bản và phổ biến nhất: useStateuseEffect. Chúng ta sẽ khám phá cách chúng hoạt động chi tiết hơn và cách sử dụng chúng để xây dựng các components động và tương tác. Đừng bỏ lỡ nhé!

Chỉ mục