Nếu bạn đã từng làm việc với React, có lẽ bạn đã gặp khó khăn trong việc quản lý trạng thái, xử lý các side effects và chia sẻ logic giữa các component. Codebase có thể trở nên cồng kềnh, và việc bảo trì cũng khó khăn hơn. Các phương pháp trước đây như: Higher-Order Components (HOC) và Render Props từng là những giải pháp phổ biến, nhưng chúng thường làm code khó đọc và phức tạp hơn.
Hooks Pattern mang đến một cách tiếp cận mới mẻ giúp giải quyết triệt để các vấn đề này. Trong bài viết này, chúng ta sẽ cùng tìm hiểu về Hooks Pattern là gì, tại sao nó quan trọng, và cách bạn có thể áp dụng nó vào ứng dụng React của mình.
1. Hooks Pattern là gì?
Hooks Pattern là một mô hình thiết kế trong React nhằm tách rời và tái sử dụng logic của các components thông qua các React Hooks. Nó cho phép các lập trình viên tách biệt logic trạng thái và hiệu ứng bên ngoài khỏi cấu trúc UI, từ đó giúp code trở nên rõ ràng, dễ bảo trì và dễ kiểm thử hơn.
Hooks Pattern tập trung vào việc sử dụng các hook tùy chỉnh (custom hooks) để giải quyết các vấn đề tái sử dụng logic mà không cần dựa vào Higher-Order Components (HOC) hoặc Render Props.
Khái niệm Hooks trở nên phổ biến sau khi React giới thiệu React Hooks trong phiên bản 16.8. Trước đây, việc quản lý state và các tính năng khác trong React đòi hỏi phải sử dụng class components, điều này đôi khi làm cho code trở nên cồng kềnh và khó hiểu. Với Hooks, bạn có thể sử dụng state và các tính năng khác của React mà không cần viết class component, giúp đơn giản hóa quá trình dev.
Để dễ hình dung hơn mình sẽ lấy ví dụ trước khi có hooks và sau khi có hooks:
// Trước khi có hooks
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
render() {
return (
<div>
<p>Bạn đã click {this.state.count} lần</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click
</button>
</div>
);
}
}
//sau khi có hooks
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Bạn đã click {count} lần</p>
<button onClick={() => setCount(count + 1)}>
Click
</button>
</div>
);
}
Như bạn cũng có thể thấy được qua 2 đoạn code trên, khi có hooks code của bạn trở nên ngắn gọn và dễ đọc hiểu hơn rất nhiều phải không nào.
2. Tại sao nên sử dụng Hooks Pattern?
Trước khi có Hooks, việc quản lý state và side effects trong các component thường phức tạp, đặc biệt là khi bạn phải làm việc với nhiều lifecycle methods như: componentDidMount
, componentDidUpdate
, componentWillUnmount
. Điều này không chỉ làm cho code trở nên dài dòng mà còn rất khó bảo trì và mở rộng.
Hooks giải quyết các vấn đề này bằng cách:
- Tách biệt logic stateful khỏi component và tái sử dụng ở nơi khác thông qua Custom Hooks, giảm thiểu sự lặp lại code và tăng tính tái sử dụng.
- Không cần phải sử dụng các class, constructor hay các lifecycle methods phức tạp. Hooks cho phép bạn sử dụng function components, làm cho code trở nên ngắn gọn và dễ hiểu hơn.
- Hooks giúp bạn chia nhỏ logic thành các hàm độc lập, đơn giản sự phức tạp trong code. Điều này cũng giúp việc kiểm thử (testing) trở nên dễ dàng hơn.
- Với việc sử dụng Hooks, code của bạn sẽ trở nên rõ ràng hơn về mặt logic và dễ dàng theo dõi, đặc biệt khi dự án phát triển ngày càng lớn dần.
Trước khi có hooks ở trong class component, nếu bạn muốn thực hiện một side effect như fetch dữ liệu khi component được mount, bạn phải sử dụng componentDidMount như này:
class DataFetcher extends React.Component {
constructor(props) {
super(props);
this.state = { data: null };
}
componentDidMount() {
fetch(this.props.url)
.then(response => response.json())
.then(json => this.setState({ data: json }));
}
render() {
// ...
}
}
Nhưng khi hooks ra đời thì code của bạn trông ngắn gọn như thế này:
function DataFetcher({ url }) {
const [data, setData] = useState(null);
useEffect(() => {
fetch(url)
.then(response => response.json())
.then(json => setData(json));
}, [url]);
// ...
}
Hooks giúp bạn viết code ngắn gọn hơn, dễ hiểu hơn, và dễ dàng quản lý state cũng như side effects trong dự án của bạn.
3. Cấu trúc của một Hook
Một Hook thực chất là một hàm JavaScript, nhưng nó tuân theo một số quy tắc đặc biệt để React có thể quản lý state và lifecycle một cách chính xác.
- Tên hàm phải bắt đầu bằng chữ "use": đây là một quy ước để React biết đó là một Hook. Ví dụ như: useState, useEffect, useCustomHook.
- Không gọi Hooks bên trong vòng lặp, điều kiện hoặc hàm lồng nhau: hooks phải được gọi ở cấp cao nhất của function component, đảm bảo rằng thứ tự gọi Hooks là như nhau trong mỗi lần render, giúp React có thể quản lý state một cách chính xác.
Đây là ví dụ sai mình thường thấy nhất, trước đây mình cũng mắc phải hihi:
function MyComponent() {
if (someCondition) {
useEffect(() => {
// ...
}); // Sai vì gọi Hook bên trong điều kiện
}
}
Gọi đúng thì như thế này:
function MyComponent() {
useEffect(() => {
if (someCondition) {
// ...
}
}); // Hook được gọi ở cấp cao nhất
}
- Chỉ gọi Hooks từ các function component hoặc custom Hook khác: không gọi Hooks từ các hàm JavaScript thông thường hoặc từ class components.
function regularFunction() {
useState(); // Sai vì gọi Hook từ hàm thông thường
}
function useCustomHook() {
const [state, setState] = useState(); // Đúng
// ...
}
Vậy thì tại sao lại phải tuân thủ các quy tắc này?
- Đảm bảo React có thể theo dõi các Hook: React sử dụng thứ tự gọi Hooks để kết hợp với state tương ứng. Nếu bạn vi phạm các quy tắc trên, thứ tự này bị phá vỡ, dẫn đến các lỗi khó debug.
- Tăng tính nhất quán và dự đoán được của code: Việc tuân thủ các quy tắc giúp code của bạn trở nên nhất quán hơn, dễ đọc và dễ bảo trì.
Dưới đây là ví dụ tạo component Counter đơn giản cho phép người dùng tăng số đếm bằng cách click vào button, để bạn hiểu rõ hơn về cách sử dụng Hooks trong dự án.
Đầu tiên, đương nhiên là sử dụng các Hook cơ bản trong React
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Bạn đã click {count} lần</p>
<button onClick={() => setCount(count + 1)}>
Click
</button>
</div>
);
}
export default Counter;
- Import useState từ React: cho phép sử dụng state trong function component.
- Khai báo state và hàm cập nhật:
const [count, setCount] = useState(0);
khởi tạo biến state count với giá trị ban đầu là 0, và hàm setCount để cập nhật giá trị của count. - Render giao diện: hiển thị số lần click và một button để tăng giá trị của count.
- Cập nhật state khi người dùng nhấn nút:
onClick={() => setCount(count + 1)}
tăng giá trị của count thêm 1 mỗi khi button được nhấn.
import React, { useState, useEffect } from 'react';
function Timer() {
const [time, setTime] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setTime(prevTime => prevTime + 1);
}, 1000);
return () => clearInterval(timer);
}, []);
return (
<div>
<p>Thời gian đã trôi qua: {time} giây</p>
</div>
);
}
export default Timer;
- Import
useEffect
từ React: cho phép thực hiện side effects trong function component. - Thiết lập một interval: sử dụng setInterval để tăng giá trị của time mỗi giây.
- Sử dụng prevTime:
setTime(prevTime => prevTime + 1);
đảm bảo luôn có giá trị mới nhất của time. - Cleanup effect: hàm trả về trong
useEffect
được gọi khi component bị unmount, giúp xóa interval và ngăn ngừa memory leak. - Mảng dependencies rỗng []: đảm bảo
useEffect
chỉ chạy một lần sau khi render.
Nâng cao hơn chút thì bạn sẽ tự custom hook riêng cho dự án của bạn. Ví dụ mình sẽ tạo một hook để lấy dữ liệu từ API
import { useState, useEffect } from 'react';
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let isMounted = true;
setLoading(true);
fetch(url)
.then(response => response.json())
.then(json => {
if (isMounted) {
setData(json);
setLoading(false);
}
})
.catch(error => {
if (isMounted) {
console.error('Error fetching data:', error);
setLoading(false);
}
});
return () => {
isMounted = false;
};
}, [url]);
return { data, loading };
}
export default useFetch;
- Quản lý state data và loading: sử dụng
useState
để quản lý dữ liệu và trạng thái loading. - Sử dụng
useEffect
để fetch dữ liệu: khi component mount hoặc url thay đổi, effect sẽ được kích hoạt. - Biến
isMounted
: đảm bảo chỉ cập nhật state khi component vẫn còn mounted, ngăn ngừa lỗi khi component đã unmount. - Cleanup function: trả về một hàm để cập nhật
isMounted
khi component unmount. - Xử lý lỗi: sử dụng
.catch
để bắt và xử lý lỗi nếu fetch dữ liệu thất bại.
import React from 'react';
import useFetch from './useFetch';
function App() {
const { data, loading } = useFetch('https://200lab.api.example.com/data');
if (loading) return <p>Loading...</p>;
return (
<div>
<h1>Dữ liệu từ API:</h1>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
}
export default App;
- Import
useFetch
: sử dụng Custom Hook vừa tạo để fetch dữ liệu. - Gọi
useFetch
với URL cần fetch: nhận về data và loading. - Kiểm tra trạng thái loading: nếu loading là true, hiển thị thông báo loading.
- Hiển thị dữ liệu: Sử dụng JSON.stringify để hiển thị
4. Lợi ích và hạn chế của Hooks Pattern
4.1 Lợi ích
- Custom Hook cho phép bạn chia sẻ logic giữa các components mà không cần sử dụng Higher-Order Components (HOCs) hay Render Props. Điều này làm cho codebase của bạn trở nên gọn gàng và dễ bảo trì hơn.
- Việc loại bỏ các class, this, và các lifecycle methods phức tạp giúp code của bạn trở nên đơn giản và dễ hiểu hơn. Bạn có thể tập trung vào logic thay vì phải quản lý các vấn đề liên quan đến cú pháp của class.
- Hooks cho phép tối ưu hóa hiệu suất thông qua việc kiểm soát khi nào effect được chạy. Bạn có thể tránh các render không cần thiết bằng cách quản lý đúng dependencies trong
useEffect
. - Vì Hooks thực chất là các hàm JavaScript, bạn có thể dễ dàng viết unit tests cho chúng mà không cần phải render toàn bộ component.
- Sử dụng Hooks khuyến khích việc sử dụng function components trong toàn bộ ứng dụng, giúp codebase trở nên nhất quán và dễ đọc hơn.
4.2 Hạn chế
- Để sử dụng Hooks một cách hiệu quả, bạn cần hiểu rõ về closures, scope, và các khái niệm nâng cao khác trong JavaScript. Nếu không, bạn có thể gặp phải các lỗi khó debug.
- Cú pháp và các quy tắc của Hooks có thể khó nắm bắt đối với các bạn mới bắt đầu, đặc biệt là việc hiểu cách hoạt động của dependencies trong
useEffect
. - Không thay thế hoàn toàn class components: mặc dù Hooks mạnh, nhưng vẫn có những trường hợp class components có thể phù hợp hơn.
- Nếu bạn không cẩn thận với việc quản lý dependencies trong
useEffect
, bạn có thể tạo ra các vòng lặp vô hạn hoặc render không cần thiết, ảnh hưởng đến hiệu suất của dự án. - Việc tạo quá nhiều Custom Hooks có thể làm cho codebase trở nên phức tạp nếu không được quản lý đúng cách. Cần cân nhắc khi trích xuất logic ra Custom Hooks.
5. Kết luận
Cá nhân mình đánh giá Hooks Pattern là một pattern mạnh, giúp đơn giản hóa việc quản lý state và side effects trong dự án. Việc hiểu và áp dụng Hooks không chỉ giúp clean code hơn, mà còn tăng khả năng tái sử dụng và bảo trì.
Một số lời khuyên của cá nhân mình:
- Nếu bạn mới bắt đầu với Hooks, hãy làm quen với
useState
vàuseEffect
trước khi chuyển sang các Hooks phức tạp hơn. - Hooks có nhiều chi tiết cần hiểu rõ, đặc biệt là về cách hoạt động của dependencies trong
useEffect
, vậy nên bạn nên đọc kỹ các tài liệu và thực hành vào dự án nhiều. - Đừng cố gắng tách tất cả logic vào Custom Hooks nếu không thật sự cần thiết. Hãy cân nhắc xem việc tạo Custom Hook có thực sự giúp code của bạn tốt hơn hay không.
Hy vọng bài viết này đã giúp bạn hiểu rõ hơn về Hooks Pattern và cách áp dụng nó vào dự án.
Các bài viết liên quan:
Bài viết liên quan
Vercel là gì? Hướng dẫn deploy dự án Next.js bằng Vercel
Dec 07, 2024 • 14 min read
So sánh giữa HOCs, Render Props và Hooks.
Dec 05, 2024 • 8 min read
Render Props pattern là gì? Hướng dẫn sử dụng Render Props
Dec 03, 2024 • 8 min read
HOCs Pattern là gì? Hướng dẫn triển khai Hocs Pattern trong dự án React
Dec 02, 2024 • 7 min read
Promise là gì? Hướng dẫn sử dụng Promise trong dự án React
Nov 27, 2024 • 7 min read
Async/await là gì? Hướng dẫn sử dụng Async/await trong dự án React
Nov 26, 2024 • 8 min read