Tạo Custom Hooks trong React: Biến Tái Sử Dụng Mã Thành Nghệ Thuật (React Roadmap)

Chào mừng bạn quay trở lại với series “React Roadmap”! Sau khi khám phá sức mạnh của useStateuseEffect – những công cụ cơ bản để quản lý trạng thái và hiệu ứng phụ trong các Functional Component – chúng ta đã có trong tay những “siêu năng lực” đầu tiên. Tuy nhiên, khi làm việc với các ứng dụng lớn hơn, bạn sẽ nhanh chóng nhận ra một vấn đề: Làm thế nào để chia sẻ những “siêu năng lực” này giữa các component một cách hiệu quả mà không phải viết lại mã?

Ví dụ, bạn có hai component khác nhau cần theo dõi trạng thái online/offline của trình duyệt, hoặc cần fetch dữ liệu từ một API cụ thể, hoặc cần xử lý logic nhập liệu phức tạp. Nếu bạn viết cùng một đoạn code useStateuseEffect trong cả hai component, bạn đang vi phạm nguyên tắc DRY (Don’t Repeat Yourself – Đừng lặp lại chính mình). Đây chính là lúc Custom Hooks tỏa sáng, biến việc tái sử dụng logic phức tạp thành một điều thanh lịch và dễ dàng.

Vấn Đề Mà Custom Hooks Giải Quyết

Trước khi Hooks ra đời, việc chia sẻ logic có trạng thái (stateful logic) giữa các component thường gặp nhiều khó khăn. Các giải pháp như Render Props hoặc Higher-Order Components (HOCs) có thể giúp tái sử dụng logic, nhưng chúng thường làm cho cấu trúc component tree trở nên phức tạp hơn (wrapper hell) và khó đọc hơn. Đặc biệt là khi bạn chỉ muốn chia sẻ một phần logic nhỏ liên quan đến state hoặc side effect.

Hãy tưởng tượng bạn có một component <ProductDetails> và một component <UserDashboard>. Cả hai đều cần fetch dữ liệu từ API và hiển thị trạng thái loading, error. Trước Hooks, bạn có thể sẽ viết đi viết lại logic fetch data, state isLoading, state error, và useEffect để thực hiện fetch trong cả hai component.

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

function ProductDetails({ productId }) {
  const [product, setProduct] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    setIsLoading(true);
    setError(null);
    fetch(`/api/products/${productId}`)
      .then(response => {
        if (!response.ok) {
          throw new Error('Network response was not ok');
        }
        return response.json();
      })
      .then(data => {
        setProduct(data);
        setIsLoading(false);
      })
      .catch(err => {
        setError(err);
        setIsLoading(false);
      });
  }, [productId]); // Dependency array

  if (isLoading) return <div>Loading product...</div>;
  if (error) return <div>Error: {error.message}</div>;
  if (!product) return null; // Or handle case where product is null initially

  return (
    <div>
      <h2>{product.name}</h2>
      <p>Price: ${product.price}</p>
      {/* ... other product details ... */}
    </div>
  );
}

// Component UserDashboard có logic fetch data tương tự...

Logic fetch data này có thể được lặp lại ở nhiều nơi. Custom Hooks ra đời để giải quyết vấn đề lặp lại logic stateful này một cách thanh lịch và hiệu quả.

Custom Hooks Là Gì?

Đơn giản mà nói, Custom Hook chỉ là một hàm JavaScript thông thường. Tuy nhiên, nó có hai đặc điểm quan trọng:

  1. **Tên hàm bắt đầu bằng tiền tố use:** Quy ước này giúp React và các công cụ linting nhận biết rằng đây là một hook.
  2. **Bên trong Custom Hook, bạn có thể gọi các Hooks khác:** Bạn có thể gọi useState, useEffect, useContext, hoặc các Custom Hooks khác mà bạn đã tạo.

Mục đích chính của Custom Hook là trích xuất (extract) logic có sử dụng các React Hooks (useState, useEffect, v.v.) ra khỏi component, giúp bạn có thể tái sử dụng logic đó ở nhiều component khác nhau mà không cần lặp lại mã. Custom Hook không trả về JSX. Nó chỉ đơn thuần là hàm chứa logic xử lý trạng thái hoặc hiệu ứng phụ, và thường trả về dữ liệu, hàm, hoặc bất cứ thứ gì mà component cần để sử dụng logic đó.

Cách Tạo Một Custom Hook Đơn Giản: useCounter

Hãy bắt đầu với một ví dụ rất đơn giản: một hook để quản lý bộ đếm.

Trước khi tạo hook, component Counter của chúng ta có thể trông như thế này:

import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  const increment = () => setCount(count + 1);
  const decrement = () => setCount(count - 1);
  const reset = () => setCount(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}

Nếu bạn cần logic đếm này ở nhiều nơi, bạn có thể trích xuất nó thành một Custom Hook:

// src/hooks/useCounter.js
import { useState } from 'react';

function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue);

  const increment = () => setCount(count + 1);
  const decrement = () => setCount(count - 1);
  const reset = () => setCount(initialValue);

  // Hook trả về những gì mà component cần sử dụng
  return {
    count,
    increment,
    decrement,
    reset
  };
}

export default useCounter;

Bây giờ, component Counter của chúng ta sẽ trở nên gọn gàng hơn nhiều:

import React from 'react';
import useCounter from './hooks/useCounter'; // Import Custom Hook

function Counter() {
  const { count, increment, decrement, reset } = useCounter(10); // Sử dụng Custom Hook

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}

Bạn có thể sử dụng useCounter trong bất kỳ Functional Component nào khác. Mỗi lần gọi useCounter, React sẽ cung cấp một “bản sao” độc lập của state count cho component đó. Đây là cách Custom Hooks giúp tái sử dụng logic stateful một cách hiệu quả.

Ví Dụ Thực Tế Hơn: useOnlineStatus

Hãy xem xét một ví dụ phức tạp hơn một chút, liên quan đến cả state và side effects: theo dõi trạng thái online/offline của trình duyệt.

Logic ban đầu trong một component:

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

function StatusBar() {
  const [isOnline, setIsOnline] = useState(navigator.onLine); // Lấy trạng thái ban đầu

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

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

    // Cleanup function để gỡ bỏ listeners khi component unmount
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []); // Dependency array rỗng: chỉ chạy 1 lần khi mount và cleanup khi unmount

  return (
    <div style={{ color: isOnline ? 'green' : 'red' }}>
      Status: {isOnline ? 'Online' : 'Offline'}
    </div>
  );
}

Để tái sử dụng logic này, chúng ta tạo Custom Hook useOnlineStatus:

// src/hooks/useOnlineStatus.js
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);

    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []); // Dependency array rỗng

  // Hook chỉ cần trả về trạng thái online/offline
  return isOnline;
}

export default useOnlineStatus;

Giờ đây, bất kỳ component nào cần biết trạng thái online đều có thể sử dụng hook này:

import React from 'react';
import useOnlineStatus from './hooks/useOnlineStatus'; // Import Custom Hook

function StatusBar() {
  const isOnline = useOnlineStatus(); // Sử dụng Custom Hook

  return (
    <div style={{ color: isOnline ? 'green' : 'red' }}>
      Status: {isOnline ? 'Online' : 'Offline'}
    </div>
  );
}

function SaveButton() {
  const isOnline = useOnlineStatus(); // Sử dụng Custom Hook ở component khác

  const handleSave = () => {
    // Logic lưu dữ liệu...
    console.log('Saving data...');
  };

  return (
    <button onClick={handleSave} disabled={!isOnline}>
      {isOnline ? 'Save Data' : 'Cannot Save (Offline)'}
    </button>
  );
}

Như bạn thấy, logic theo dõi trạng thái online đã được đóng gói gọn gàng trong useOnlineStatus. Hai component StatusBarSaveButton chỉ đơn giản gọi hook và sử dụng giá trị trả về, giúp chúng dễ đọc, dễ hiểu và dễ bảo trì hơn rất nhiều.

Các Quy Tắc Của Hooks (Rules of Hooks)

Khi làm việc với Hooks (bao gồm cả Built-in Hooks và Custom Hooks), bạn phải tuân thủ hai quy tắc quan trọng để đảm bảo React có thể quản lý state và effects một cách chính xác:

  1. **Chỉ gọi Hooks ở Top Level:** Không gọi Hooks bên trong vòng lặp (loops), điều kiện (conditions), hoặc các hàm lồng nhau (nested functions). Điều này đảm bảo Hooks luôn được gọi theo cùng một thứ tự trong mỗi lần component render.
  2. **Chỉ gọi Hooks từ React Functions:** Chỉ gọi Hooks từ Functional Component hoặc từ Custom Hooks khác. Không gọi Hooks từ các hàm JavaScript thông thường hoặc từ Class Component.

Vi phạm các quy tắc này có thể dẫn đến hành vi không mong muốn và khó gỡ lỗi. May mắn thay, có một plugin ESLint tuyệt vời là eslint-plugin-react-hooks giúp bạn tự động kiểm tra và cảnh báo nếu bạn vi phạm các quy tắc này trong quá trình phát triển.

Lợi Ích Của Custom Hooks

Việc sử dụng Custom Hooks mang lại nhiều lợi ích đáng kể:

  • **Tái Sử Dụng Logic:** Đây là lợi ích chính. Bạn đóng gói logic stateful (sử dụng useState, useEffect, etc.) vào một hàm duy nhất và sử dụng lại ở nhiều component.
  • **Mã Sạch Hơn, Dễ Đọc Hơn:** Bằng cách trích xuất logic phức tạp ra khỏi component, component của bạn trở nên tập trung hơn vào việc hiển thị giao diện (JSX và cách sử dụng dữ liệu từ hook), trong khi hook tập trung vào logic xử lý dữ liệu.
  • **Dễ Bảo Trì:** Khi cần thay đổi hoặc sửa lỗi logic, bạn chỉ cần làm điều đó ở một nơi duy nhất (trong Custom Hook) thay vì phải cập nhật ở nhiều component khác nhau.
  • **Dễ Kiểm Thử (Test):** Bạn có thể kiểm thử logic bên trong Custom Hook một cách độc lập, không cần render component.
  • **Tách Biệt Các Mối Quan Tâm (Separation of Concerns):** Custom Hooks giúp tách biệt rõ ràng logic xử lý dữ liệu và trạng thái khỏi logic hiển thị giao diện.

So với các kỹ thuật tái sử dụng logic trước đây như HOCs hay Render Props (mà chúng ta đã tìm hiểu ở bài trước tại đây), Custom Hooks thường đơn giản và trực quan hơn nhiều khi mục tiêu là chia sẻ logic stateful.

So Sánh: Trước và Sau khi dùng Custom Hooks

Bảng sau đây tóm tắt sự khác biệt và lợi ích khi sử dụng Custom Hooks:

Đặc Điểm Trước Custom Hooks (Lặp lại logic stateful) Với Custom Hooks
Tái sử dụng Logic stateful (useState, useEffect, v.v.) Phải viết lại cùng một đoạn code trong nhiều component. Logic được đóng gói trong một hàm hook duy nhất và sử dụng lại.
Độ phức tạp của Component Tree Không ảnh hưởng trực tiếp, nhưng component có thể trở nên cồng kềnh với nhiều logic. Giảm thiểu “wrapper hell” so với HOCs/Render Props khi chỉ chia sẻ logic stateful.
Khả năng đọc Component chứa nhiều logic state và effect, khó phân biệt đâu là logic hiển thị, đâu là logic nghiệp vụ. Component gọn gàng, tập trung vào hiển thị. Logic phức tạp được trừu tượng hóa vào hook.
Bảo trì và Sửa lỗi Cần cập nhật logic ở nhiều nơi. Nguy cơ sai sót, quên cập nhật. Chỉ cần cập nhật logic ở một nơi duy nhất (trong hook).
Kiểm thử (Testing) Thường phải kiểm thử logic thông qua component. Có thể kiểm thử logic của hook một cách độc lập.
Mức độ trừu tượng hóa Thấp hơn đối với logic stateful. Cao hơn, đóng gói logic thành các đơn vị tái sử dụng có ý nghĩa.

Khi Nào Nên Tạo Custom Hook?

Bạn nên xem xét tạo Custom Hook khi bạn nhận thấy mình đang lặp lại cùng một tổ hợp useState, useEffect, hoặc các hook khác để quản lý một loại logic cụ thể (ví dụ: fetch data, form handling, subscription, timer, v.v.) ở nhiều component khác nhau. Nếu một đoạn logic chỉ được sử dụng ở một component duy nhất và không có ý định tái sử dụng, bạn không nhất thiết phải trích xuất nó thành hook; việc giữ nó trong component đó vẫn hoàn toàn ổn.

Kết Luận

Custom Hooks là một tính năng vô cùng mạnh mẽ và là một phần không thể thiếu trong việc phát triển ứng dụng React hiện đại. Chúng cho phép chúng ta trích xuất, đóng gói và tái sử dụng logic stateful một cách thanh lịch, giúp mã nguồn gọn gàng hơn, dễ đọc, dễ bảo trì và dễ kiểm thử hơn. Nắm vững cách tạo và sử dụng Custom Hooks sẽ nâng cao đáng kể khả năng phát triển React của bạn.

Trong bài viết này, chúng ta đã hiểu Custom Hooks là gì, tại sao chúng quan trọng, cách tạo một hook đơn giản và một ví dụ thực tế hơn, cũng như các quy tắc cần tuân thủ. Hãy bắt đầu tìm kiếm những đoạn logic lặp lại trong các project của bạn và thử biến chúng thành Custom Hooks. Bạn sẽ thấy việc tái sử dụng mã trong React trở nên dễ dàng và thú vị hơn bao giờ hết!

Đừng quên theo dõi series React Roadmap để tiếp tục khám phá những khía cạnh sâu sắc hơn của React nhé!

Hẹn gặp lại trong bài viết tiếp theo!

Chỉ mục