Tiếng Việt

Phân tích sâu về kiến trúc component của React, so sánh composition và inheritance. Tìm hiểu lý do React ưu tiên composition và khám phá các pattern như HOC, Render Props, và Hooks để xây dựng component có thể tái sử dụng và mở rộng.

Kiến trúc Component trong React: Tại sao Composition Vượt trội hơn Inheritance

Trong thế giới phát triển phần mềm, kiến trúc là tối quan trọng. Cách chúng ta cấu trúc code sẽ quyết định khả năng mở rộng, bảo trì và tái sử dụng của nó. Đối với các lập trình viên làm việc với React, một trong những quyết định kiến trúc cơ bản nhất xoay quanh cách chia sẻ logic và UI giữa các component. Điều này đưa chúng ta đến một cuộc tranh luận kinh điển trong lập trình hướng đối tượng, được tái hiện trong thế giới dựa trên component của React: Composition (Kết hợp) vs. Inheritance (Kế thừa).

Nếu bạn có nền tảng từ các ngôn ngữ hướng đối tượng cổ điển như Java hay C++, kế thừa có thể là lựa chọn tự nhiên đầu tiên. Đó là một khái niệm mạnh mẽ để tạo ra các mối quan hệ 'is-a' (là một). Tuy nhiên, tài liệu chính thức của React đưa ra một khuyến nghị rõ ràng và mạnh mẽ: "Tại Facebook, chúng tôi sử dụng React trong hàng nghìn component và chúng tôi chưa tìm thấy bất kỳ trường hợp sử dụng nào mà chúng tôi đề xuất tạo ra các hệ thống phân cấp kế thừa component."

Bài viết này sẽ cung cấp một khám phá toàn diện về lựa chọn kiến trúc này. Chúng ta sẽ phân tích ý nghĩa của kế thừa và kết hợp trong bối cảnh React, chứng minh tại sao kết hợp là phương pháp thành ngữ và vượt trội hơn, đồng thời khám phá các mẫu mạnh mẽ—từ Higher-Order Components đến Hooks hiện đại—giúp kết hợp trở thành người bạn tốt nhất của lập trình viên để xây dựng các ứng dụng mạnh mẽ và linh hoạt cho người dùng toàn cầu.

Tìm hiểu về Lối cũ: Kế thừa là gì?

Kế thừa là một trụ cột cốt lõi của Lập trình Hướng đối tượng (OOP). Nó cho phép một lớp mới (lớp con) thừa hưởng các thuộc tính và phương thức của một lớp đã có (lớp cha). Điều này tạo ra một mối quan hệ 'is-a' (là một) chặt chẽ. Ví dụ, một GoldenRetriever là một Dog, mà Dog lại là một Animal.

Kế thừa trong Bối cảnh ngoài React

Hãy xem một ví dụ lớp JavaScript đơn giản để củng cố khái niệm:

class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(`${this.name} makes a noise.`);
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name); // Gọi hàm khởi tạo của lớp cha
    this.breed = breed;
  }

  speak() { // Ghi đè phương thức của lớp cha
    console.log(`${this.name} barks.`);
  }

  fetch() {
    console.log(`${this.name} is fetching the ball!`);
  }
}

const myDog = new Dog('Buddy', 'Golden Retriever');
myDog.speak(); // Output: "Buddy barks."
myDog.fetch(); // Output: "Buddy is fetching the ball!"

Trong mô hình này, lớp Dog tự động nhận được thuộc tính name và phương thức speak từ Animal. Nó cũng có thể thêm các phương thức riêng của mình (fetch) và ghi đè các phương thức đã có. Điều này tạo ra một hệ thống phân cấp cứng nhắc.

Tại sao Kế thừa lại Thất bại trong React

Mặc dù mô hình 'is-a' này hoạt động tốt với một số cấu trúc dữ liệu, nó lại tạo ra những vấn đề đáng kể khi áp dụng cho các component UI trong React:

Vì những vấn đề này, đội ngũ React đã thiết kế thư viện xung quanh một mô hình linh hoạt và mạnh mẽ hơn: kết hợp (composition).

Tiếp nhận Phong cách React: Sức mạnh của Composition

Composition (kết hợp) là một nguyên tắc thiết kế ưu tiên mối quan hệ 'has-a' (có một) hoặc 'uses-a' (sử dụng một). Thay vì một component một component khác, nó các component khác hoặc sử dụng chức năng của chúng. Các component được coi như những khối xây dựng—giống như các mảnh LEGO—có thể được kết hợp theo nhiều cách khác nhau để tạo ra các giao diện người dùng phức tạp mà không bị khóa vào một hệ thống phân cấp cứng nhắc.

Mô hình kết hợp của React vô cùng linh hoạt, và nó thể hiện qua một số mẫu chính. Hãy cùng khám phá chúng, từ những mẫu cơ bản nhất đến những mẫu hiện đại và mạnh mẽ nhất.

Kỹ thuật 1: Chứa đựng (Containment) với `props.children`

Hình thức kết hợp đơn giản nhất là chứa đựng. Đây là nơi một component hoạt động như một vùng chứa chung hoặc 'hộp', và nội dung của nó được truyền vào từ một component cha. React có một prop đặc biệt, được tích hợp sẵn cho việc này: props.children.

Hãy tưởng tượng bạn cần một component `Card` có thể bao bọc bất kỳ nội dung nào với một đường viền và bóng đổ nhất quán. Thay vì tạo ra các biến thể `TextCard`, `ImageCard`, và `ProfileCard` thông qua kế thừa, bạn tạo ra một component `Card` chung.

// Card.js - Một component chứa chung
function Card(props) {
  return (
    <div className="card">
      {props.children}
    </div>
  );
}

// App.js - Sử dụng component Card
function App() {
  return (
    <div>
      <Card>
        <h1>Chào mừng!</h1>
        <p>Nội dung này nằm bên trong một component Card.</p>
      </Card>

      <Card>
        <img src="/path/to/image.jpg" alt="Một hình ảnh ví dụ" />
        <p>Đây là một card chứa hình ảnh.</p>
      </Card>
    </div>
  );
}

Ở đây, component Card không biết hoặc không quan tâm nó chứa gì. Nó chỉ đơn giản là cung cấp kiểu dáng bao bọc. Nội dung giữa các thẻ mở và đóng <Card> được tự động truyền vào dưới dạng props.children. Đây là một ví dụ tuyệt vời về sự tách rời và khả năng tái sử dụng.

Kỹ thuật 2: Chuyên môn hóa (Specialization) với Props

Đôi khi, một component cần nhiều 'lỗ hổng' để được lấp đầy bởi các component khác. Mặc dù bạn có thể sử dụng props.children, một cách rõ ràng và có cấu trúc hơn là truyền các component dưới dạng props thông thường. Mẫu này thường được gọi là chuyên môn hóa.

Hãy xem xét một component Modal. Một modal thường có một phần tiêu đề, một phần nội dung, và một phần hành động (với các nút như "Xác nhận" hoặc "Hủy"). Chúng ta có thể thiết kế Modal của mình để chấp nhận các phần này dưới dạng props.

// Modal.js - Một vùng chứa chuyên biệt hơn
function Modal(props) {
  return (
    <div className="modal-backdrop">
      <div className="modal-content">
        <div className="modal-header">{props.title}</div>
        <div className="modal-body">{props.body}</div>
        <div className="modal-footer">{props.actions}</div>
      </div>
    </div>
  );
}

// App.js - Sử dụng Modal với các component cụ thể
function App() {
  const confirmationTitle = <h2>Xác nhận Hành động</h2>;
  const confirmationBody = <p>Bạn có chắc chắn muốn tiếp tục với hành động này không?</p>;
  const confirmationActions = (
    <div>
      <button>Xác nhận</button>
      <button>Hủy</button>
    </div>
  );

  return (
    <Modal
      title={confirmationTitle}
      body={confirmationBody}
      actions={confirmationActions}
    />
  );
}

Trong ví dụ này, Modal là một component bố cục có khả năng tái sử dụng cao. Chúng ta chuyên môn hóa nó bằng cách truyền vào các phần tử JSX cụ thể cho `title`, `body`, và `actions`. Điều này linh hoạt hơn nhiều so với việc tạo ra các lớp con `ConfirmationModal` và `WarningModal`. Chúng ta chỉ đơn giản là kết hợp Modal với các nội dung khác nhau khi cần.

Kỹ thuật 3: Higher-Order Components (HOCs)

Để chia sẻ logic không liên quan đến UI, chẳng hạn như tìm nạp dữ liệu, xác thực, hoặc ghi log, các lập trình viên React trong quá khứ đã chuyển sang một mẫu gọi là Higher-Order Components (HOCs). Mặc dù phần lớn đã được thay thế bởi Hooks trong React hiện đại, việc hiểu chúng là rất quan trọng vì chúng đại diện cho một bước tiến hóa quan trọng trong câu chuyện về composition của React và vẫn còn tồn tại trong nhiều codebase.

HOC là một hàm nhận một component làm đối số và trả về một component mới, đã được tăng cường.

Hãy tạo một HOC có tên là `withLogger` để ghi log các props của một component mỗi khi nó cập nhật. Điều này rất hữu ích cho việc gỡ lỗi.

// withLogger.js - HOC
import React, { useEffect } from 'react';

function withLogger(WrappedComponent) {
  // Nó trả về một component mới...
  return function EnhancedComponent(props) {
    useEffect(() => {
      console.log('Component đã được cập nhật với props mới:', props);
    }, [props]);

    // ... render component gốc với các props ban đầu.
    return <WrappedComponent {...props} />;
  };
}

// MyComponent.js - Một component cần được tăng cường
function MyComponent({ name, age }) {
  return (
    <div>
      <h1>Xin chào, {name}!</h1>
      <p>Bạn {age} tuổi.</p>
    </div>
  );
}

// Xuất component đã được tăng cường
export default withLogger(MyComponent);

Hàm `withLogger` bao bọc `MyComponent`, cung cấp cho nó khả năng ghi log mới mà không sửa đổi code nội bộ của `MyComponent`. Chúng ta có thể áp dụng cùng HOC này cho bất kỳ component nào khác để cung cấp cho nó tính năng ghi log tương tự.

Thách thức với HOCs:

Kỹ thuật 4: Render Props

Mẫu Render Prop ra đời như một giải pháp cho một số thiếu sót của HOCs. Nó cung cấp một cách chia sẻ logic rõ ràng hơn.

Một component với render prop sẽ nhận một hàm làm prop (thường được đặt tên là `render`) và gọi hàm đó để quyết định sẽ render cái gì, truyền bất kỳ trạng thái hoặc logic nào làm đối số cho nó.

Hãy tạo một component `MouseTracker` theo dõi tọa độ X và Y của chuột và cung cấp chúng cho bất kỳ component nào muốn sử dụng.

// MouseTracker.js - Component với render prop
import React, { useState, useEffect } from 'react';

function MouseTracker({ render }) {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  const handleMouseMove = (event) => {
    setPosition({ x: event.clientX, y: event.clientY });
  };

  useEffect(() => {
    window.addEventListener('mousemove', handleMouseMove);
    return () => {
      window.removeEventListener('mousemove', handleMouseMove);
    };
  }, []);

  // Gọi hàm render với state
  return render(position);
}

// App.js - Sử dụng MouseTracker
function App() {
  return (
    <div>
      <h1>Di chuyển chuột của bạn!</h1>
      <MouseTracker
        render={mousePosition => (
          <p>Vị trí chuột hiện tại là ({mousePosition.x}, {mousePosition.y})</p>
        )}
      />
    </div>
  );
}

Ở đây, `MouseTracker` đóng gói tất cả logic để theo dõi chuyển động của chuột. Nó không tự render bất cứ thứ gì. Thay vào đó, nó ủy thác logic render cho prop `render` của nó. Điều này rõ ràng hơn HOCs vì bạn có thể thấy chính xác dữ liệu `mousePosition` đến từ đâu ngay bên trong JSX.

Prop `children` cũng có thể được sử dụng như một hàm, đây là một biến thể phổ biến và thanh lịch của mẫu này:

// Sử dụng children như một hàm
<MouseTracker>
  {mousePosition => (
    <p>Vị trí chuột hiện tại là ({mousePosition.x}, {mousePosition.y})</p>
  )}
</MouseTracker>

Kỹ thuật 5: Hooks (Phương pháp Hiện đại và được Ưa chuộng)

Được giới thiệu trong React 16.8, Hooks đã cách mạng hóa cách chúng ta viết các component React. Chúng cho phép bạn sử dụng state và các tính năng khác của React trong các functional component. Quan trọng nhất, custom Hooks cung cấp giải pháp thanh lịch và trực tiếp nhất để chia sẻ logic có trạng thái (stateful logic) giữa các component.

Hooks giải quyết các vấn đề của HOCs và Render Props một cách gọn gàng hơn nhiều. Hãy tái cấu trúc ví dụ `MouseTracker` của chúng ta thành một custom hook có tên là `useMousePosition`.

// hooks/useMousePosition.js - Một Hook tùy chỉnh
import { useState, useEffect } from 'react';

export function useMousePosition() {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    const handleMouseMove = (event) => {
      setPosition({ x: event.clientX, y: event.clientY });
    };

    window.addEventListener('mousemove', handleMouseMove);
    return () => {
      window.removeEventListener('mousemove', handleMouseMove);
    };
  }, []); // Mảng phụ thuộc rỗng có nghĩa là effect này chỉ chạy một lần

  return position;
}

// DisplayMousePosition.js - Một component sử dụng Hook
import { useMousePosition } from './hooks/useMousePosition';

function DisplayMousePosition() {
  const position = useMousePosition(); // Chỉ cần gọi hook!

  return (
    <p>
      Vị trí chuột là ({position.x}, {position.y})
    </p>
  );
}

// Một component khác, có thể là một phần tử tương tác
import { useMousePosition } from './hooks/useMousePosition';

function InteractiveBox() {
  const { x, y } = useMousePosition();

  const style = {
    position: 'absolute',
    top: y - 25, // Căn giữa hộp vào con trỏ chuột
    left: x - 25,
    width: '50px',
    height: '50px',
    backgroundColor: 'lightblue',
  };

  return <div style={style} />;
}

Đây là một cải tiến vượt bậc. Không có 'địa ngục bao bọc', không có xung đột tên prop, và không có các hàm render prop phức tạp. Logic được tách biệt hoàn toàn vào một hàm có thể tái sử dụng (`useMousePosition`), và bất kỳ component nào cũng có thể 'móc vào' logic có trạng thái đó bằng một dòng code duy nhất, rõ ràng. Custom Hooks là biểu hiện cuối cùng của composition trong React hiện đại, cho phép bạn xây dựng thư viện các khối logic tái sử dụng của riêng mình.

So sánh nhanh: Composition và Inheritance trong React

Để tóm tắt những khác biệt chính trong bối cảnh React, đây là một so sánh trực tiếp:

Khía cạnh Kế thừa (Anti-Pattern trong React) Kết hợp (Ưu tiên trong React)
Mối quan hệ Mối quan hệ 'is-a' (là một). Một component chuyên biệt là một phiên bản của một component cơ sở. Mối quan hệ 'has-a' (có một) hoặc 'uses-a' (sử dụng một). Một component phức tạp các component nhỏ hơn hoặc sử dụng logic được chia sẻ.
Khớp nối Cao. Các component con được khớp nối chặt chẽ với việc triển khai của cha chúng. Thấp. Các component độc lập và có thể được tái sử dụng trong các bối cảnh khác nhau mà không cần sửa đổi.
Linh hoạt Thấp. Hệ thống phân cấp cứng nhắc dựa trên lớp khiến việc chia sẻ logic giữa các cây component khác nhau trở nên khó khăn. Cao. Logic và UI có thể được kết hợp và tái sử dụng theo vô số cách, giống như các khối xây dựng.
Khả năng tái sử dụng Code Bị giới hạn trong hệ thống phân cấp được xác định trước. Bạn nhận được cả "con khỉ đột" khi bạn chỉ muốn "quả chuối". Tuyệt vời. Các component và hook nhỏ, tập trung có thể được sử dụng trên toàn bộ ứng dụng.
Thành ngữ React Không được khuyến khích bởi đội ngũ React chính thức. Phương pháp được đề xuất và là thành ngữ để xây dựng các ứng dụng React.

Kết luận: Hãy Tư duy theo Lối Composition

Cuộc tranh luận giữa kết hợp và kế thừa là một chủ đề nền tảng trong thiết kế phần mềm. Mặc dù kế thừa có vị trí của nó trong OOP cổ điển, bản chất năng động, dựa trên component của việc phát triển UI khiến nó không phù hợp với React. Thư viện này được thiết kế cơ bản để tận dụng composition.

Bằng cách ưu tiên composition, bạn sẽ có được:

Là một nhà phát triển React toàn cầu, việc thành thạo composition không chỉ là tuân theo các phương pháp hay nhất—mà còn là hiểu được triết lý cốt lõi làm cho React trở thành một công cụ mạnh mẽ và hiệu quả. Hãy bắt đầu bằng cách tạo ra các component nhỏ, tập trung. Sử dụng props.children cho các vùng chứa chung và props để chuyên môn hóa. Để chia sẻ logic, hãy tìm đến custom Hooks trước tiên. Bằng cách tư duy theo lối composition, bạn sẽ đi đúng hướng để xây dựng các ứng dụng React thanh lịch, mạnh mẽ và có khả năng mở rộng, trường tồn với thời gian.