Mục lục
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 Components và Functional 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>
<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ằngthis.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óathis
, 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óathis
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ứcrender
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>
<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ư
useState
vàuseEffect
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ộtuseEffect
). - 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 bindingthis
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ặcstatic 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ư:
- Chuyển cấu trúc class thành một hàm.
- Thay thế
this.state
vàthis.setState
bằnguseState
. - Ánh xạ logic từ các phương thức vòng đời (
componentDidMount
,componentDidUpdate
,componentWillUnmount
) sang một hoặc nhiều hookuseEffect
với dependency array phù hợp và hàm cleanup. - Thay thế việc sử dụng
this.props
bằng đối sốprops
của hàm. - Đó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: useState
và useEffect
. 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é!