Custom hook trong React: tách và tái sử dụng logic giữa các component

  • author-image

    Nguyễn Đình Cường

  • blog-tag
  • blog-comment 0 comment
  • created-date 09 Nov, 2026
blog-thumbnail

Khi hai component cùng cần một đoạn logic có state hoặc effect, như lấy dữ liệu, đồng bộ localStorage hay lắng nghe cửa sổ, sao chép mã là không ổn. React cho phép bạn đóng gói logic đó vào custom hook, một hàm bình thường có thể dùng các hook khác bên trong.

Custom hook thực chất là gì?

Đó là một hàm JavaScript có tên bắt đầu bằng use và gọi các hook khác như useState hay useEffect. Không có phép màu nào: mỗi lần dùng, mỗi component có bản state riêng. Hai component dùng chung một hook không chia sẻ state, chỉ chia sẻ cách làm.

Tiền tố use không chỉ là quy ước: công cụ kiểm tra và React dựa vào nó để áp dụng đúng quy tắc hook.

Ví dụ 1: useLocalStorage

Giả sử nhiều nơi cần lưu một giá trị vào localStorage và đồng bộ với state:

import { useState, useEffect } from 'react';

export function useLocalStorage(khoa, macDinh) {
  const [giaTri, setGiaTri] = useState(() => {
    try {
      const daLuu = localStorage.getItem(khoa);
      return daLuu !== null ? JSON.parse(daLuu) : macDinh;
    } catch {
      return macDinh;
    }
  });

  useEffect(() => {
    try {
      localStorage.setItem(khoa, JSON.stringify(giaTri));
    } catch {
      // bỏ qua nếu không ghi được
    }
  }, [khoa, giaTri]);

  return [giaTri, setGiaTri];
}

Cách dùng hệt như useState:

function CaiDat() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  return (
    <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
      Đang dùng: {theme}
    </button>
  );
}

Chú ý cách khởi tạo state bằng hàm (lazy initial state): hàm chỉ chạy ở lần render đầu, tránh đọc localStorage mỗi lần render.

Ví dụ 2: useFetch

export function useFetch(url) {
  const [du_lieu, setDuLieu] = useState(null);
  const [loi, setLoi] = useState(null);
  const [dangTai, setDangTai] = useState(true);

  useEffect(() => {
    const controller = new AbortController();
    setDangTai(true);
    setLoi(null);
    fetch(url, { signal: controller.signal })
      .then(r => {
        if (!r.ok) throw new Error('Lỗi ' + r.status);
        return r.json();
      })
      .then(setDuLieu)
      .catch(e => { if (e.name !== 'AbortError') setLoi(e.message); })
      .finally(() => setDangTai(false));
    return () => controller.abort();
  }, [url]);

  return { du_lieu, loi, dangTai };
}

Component gọi hook trở nên rất gọn:

function DanhSach() {
  const { du_lieu, loi, dangTai } = useFetch('/api/khoa-hoc');
  if (dangTai) return <p>Đang tải...</p>;
  if (loi) return <p>{loi}</p>;
  return <ul>{du_lieu.map(k => <li key={k.id}>{k.ten}</li>)}</ul>;
}

Hook này cho mục đích học tập. Khi làm sản phẩm thật, nhu cầu như cache, thử lại và đồng bộ thường được giao cho thư viện chuyên dụng.

Ví dụ 3: useDebounce

export function useDebounce(giaTri, doTre = 300) {
  const [giaTriTre, setGiaTriTre] = useState(giaTri);
  useEffect(() => {
    const id = setTimeout(() => setGiaTriTre(giaTri), doTre);
    return () => clearTimeout(id);
  }, [giaTri, doTre]);
  return giaTriTre;
}

// ô tìm kiếm chỉ gọi API khi người dùng ngừng gõ 300ms
const [tuKhoa, setTuKhoa] = useState('');
const tuKhoaTre = useDebounce(tuKhoa, 300);
const { du_lieu } = useFetch('/api/tim?q=' + encodeURIComponent(tuKhoaTre));

Ba hook nhỏ này ghép lại với nhau tạo ô tìm kiếm hoàn chỉnh mà mỗi mảnh đều dễ hiểu và dùng lại được.

Quy tắc của hook cần nhớ

  1. Chỉ gọi hook ở cấp cao nhất của component hoặc custom hook, không gọi trong vòng lặp, điều kiện hay hàm lồng.
  2. Chỉ gọi hook từ component hàm hoặc từ custom hook khác, không gọi từ hàm JavaScript thông thường.
  3. Đặt tên bắt đầu bằng use.

Khi nào nên tạo custom hook?

  • Cùng một tổ hợp state và effect xuất hiện ở từ hai nơi trở lên.
  • Component quá dài và logic có thể tách ra thành khối có tên rõ nghĩa.
  • Bạn muốn kiểm thử logic riêng biệt khỏi giao diện.

Đừng tạo hook chỉ để bọc một dòng useState. Hook tốt có tên mô tả đúng mục đích, giao diện sử dụng đơn giản (đối số vào, giá trị ra) và che giấu được độ phức tạp bên trong.

Kiểm thử và đặt tên

Một lợi ích ít được nhắc của custom hook là khả năng kiểm thử: logic nằm ngoài giao diện nên dễ viết test riêng, ví dụ bằng thư viện React Testing Library với tiện ích render hook. Về đặt tên, hãy dùng danh từ hoặc cụm động từ mô tả kết quả như useChieuRong, useDebounce, useDanhSachKhoaHoc. Tránh những cái tên mơ hồ như useData hay useStuff, vì khi dự án có hàng chục hook, tên rõ nghĩa là tài liệu tốt nhất.

Cuối cùng, hãy nhớ rằng mỗi lần gọi hook tạo state riêng. Nếu bạn muốn chia sẻ state thật sự giữa nhiều component, cần dùng Context hoặc một thư viện quản lý trạng thái, không phải chỉ custom hook.

Kết luận

Custom hook là cách tái sử dụng logic có trạng thái trong React: viết hàm bắt đầu bằng use, gọi các hook khác bên trong, trả về những gì component cần. Hãy bắt đầu bằng cách tìm trong dự án của bạn đoạn effect nào bị lặp và gom nó lại.

Để học React theo cách có hệ thống từ nền tảng đến các mẫu nâng cao, bạn có thể xem các khóa học tại codebem.com.

author_photo
Nguyễn Đình Cường

0 comment