React Hooks — chúng là gì, hook chính và quy tắc sử dụng

Tác giả: IT Sectr Đã đăng: 2026-07-04 Thời gian đọc: 10 phút

React Hooks là các hàm cho phép bạn sử dụng state, effect, context và các tính năng React khác trong component hàm, mà không cần viết class. Được giới thiệu trong React 16.8, hook đã thay đổi hoàn toàn cách tiếp cận phát triển, thay thế các pattern phức tạp như HOC và render props. Theo React, 2024, có 15 hook tích hợp sẵn bao phủ mọi tình huống quản lý component.

Điểm chính

  • Hook — hàm để kết nối tính năng React với component hàm mà không cần extends Component.
  • useState — hook cơ bản để quản lý state, trả về giá trị và một setter.
  • useEffect — dành cho side effect: yêu cầu API, đăng ký, timer, thao tác DOM.
  • useContext — để truy cập context React mà không cần wrapper Consumer và static contextType.
  • Quy tắc hook — chỉ gọi ở cấp cao nhất và chỉ trong component hàm hoặc hook tùy chỉnh.

React Hooks là gì?

React Hooks là một tập hợp các hàm được tích hợp trong React cho phép bạn “kết nối” vào các cơ chế nội bộ của framework mà không cần tạo class. Trước React 16.8, component hàm chỉ có thể nhận props và trả về JSX — bất kỳ logic phức tạp nào cũng yêu cầu một component class.

Vấn đề với component class là sự phân chia logic liên quan. Mã liên quan đến một thực thể (ví dụ, đăng ký WebSocket) bị chia cắt giữa componentDidMount, componentDidUpdate và componentWillUnmount. Hook giải quyết điều này: tất cả logic được nhóm trong một useEffect duy nhất.

Vấn đề thứ hai là tái sử dụng logic. Để tái sử dụng logic state giữa các component class, bạn phải áp dụng HOC (Component cấp cao) hoặc render props — cả hai pattern đều tạo ra sự lồng ghép thừa trong cây component. Hook tùy chỉnh giải quyết điều này mà không cần lồng ghép.

useState — Quản lý state

useState là hook cơ bản nhất. Nó nhận một giá trị khởi tạo và trả về một tuple: giá trị hiện tại và một hàm để cập nhật nó. Không giống this.setState trong class, useState không hợp nhất các đối tượng — state mới thay thế hoàn toàn state trước đó cho một lần gọi useState cụ thể.

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

function Form() {
  const [text, setText] = useState('');
  const [submitting, setSubmitting] = useState(false);

  async function handleSubmit() {
    setSubmitting(true);
    await fetch('/api/submit', {
      method: 'POST',
      body: JSON.stringify({ text })
    });
    setSubmitting(false);
  }

  return (
    <div>
      <input
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <button onClick={handleSubmit}
        disabled={submitting}>
        {submitting ? 'Saving...' : 'Submit'}
      </button>
    </div>
  );
}

Dạng hàm của setState

Setter của useState có thể nhận một hàm nhận giá trị trước đó và trả về giá trị mới. Dạng hàm là bắt buộc khi state mới được tính từ state trước đó và các lần gọi setState theo lô có thể xảy ra — mỗi lần gọi sẽ nhận được giá trị trước hiện tại.

useEffect — Side Effect và vòng đời

useEffect là hook để thực hiện side effect trong component hàm. Nó nhận hai đối số: một hàm effect và một mảng phụ thuộc. React chạy effect sau mỗi lần render, nhưng khi có phụ thuộc được chỉ định — chỉ khi một trong số chúng thay đổi.

Một mảng phụ thuộc rỗng [] có nghĩa là effect chạy một lần sau lần render đầu tiên (tương tự componentDidMount). Trả về một hàm từ effect là một hàm dọn dẹp chạy khi unmount (tương tự componentWillUnmount) và trước khi chạy lại effect.

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

function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);

  useEffect(() => {
    const socket = new WebSocket(`wss://chat/${roomId}`);

    socket.onmessage = (event) => {
      setMessages(prev => [...prev, JSON.parse(event.data)]);
    };

    // Dọn dẹp - đóng socket
    return () => socket.close();
  }, [roomId]);

  return <ul>
    {messages.map((msg, i) =>
      <li key={i}>{msg.text}</li>
    )}
  </ul>;
}

Các phụ thuộc của useEffect là kết nối duy nhất giữa effect và thế giới bên ngoài. Nếu effect sử dụng một biến từ props hoặc state, nó phải có trong mảng phụ thuộc. Bỏ qua quy tắc này là nguồn gốc của lỗi với stale closure.

useContext — Truy cập context

useContext là hook để đọc giá trị context. Nó nhận một đối tượng context được tạo bởi React.createContext và trả về giá trị hiện tại. Nếu context thay đổi, component tự động render lại với giá trị mới.

Trước useContext, truy cập context trong component hàm yêu cầu bọc trong Context.Consumer, tạo ra lồng ghép. useContext loại bỏ hoàn toàn Consumer — chỉ cần gọi hook ở bất kỳ đâu trong component hàm.

js
import React, { createContext, useContext } from 'react';

const ThemeContext = createContext('sáng');

function ThemedButton() {
  const theme = useContext(ThemeContext);

  return <button
    className={`btn btn-${theme}`}
  >
    {theme === 'dark' ? 'moon' : '☀️'}
  </button>;
}

useContext không phải là sự thay thế cho Redux hay state toàn cục. React khuyến nghị sử dụng context cho các giá trị hiếm khi thay đổi — chủ đề, ngôn ngữ, trạng thái xác thực. Đối với dữ liệu thường xuyên cập nhật, mỗi lần render lại sẽ tính toán lại tất cả consumer context, điều này có thể làm giảm hiệu suất.

useReducer — Logic state phức tạp

useReducer là một thay thế cho useState dành cho state có logic cập nhật phức tạp. Nó nhận một hàm reducer và state ban đầu, trả về state hiện tại và một hàm dispatch. Reducer nhận state hiện tại và action, trả về state mới — một hàm thuần túy không có side effect.

useReducer đặc biệt hữu ích khi nhiều action (set, reset, increment, addItem) quản lý một state duy nhất. Thay vì nhiều useState với các setter khác nhau — một reducer duy nhất với các loại action rõ ràng. Điều này làm cho logic có thể dự đoán được và dễ kiểm thử.

js
import React, { useReducer } from 'react';

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { ...state, count: state.count + 1 };
    case 'setStep':
      return { ...state, step: action.step };
    case 'reset':
      return { count: 0, step: 1 };
    default:
      throw new Error('Unknown action: ' + action.type);
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, {
    count: 0,
    step: 1
  });

  return (
    <>
      <p>Count: {state.count}</p>
      <button
        onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button
        onClick={() => dispatch({ type: 'reset' })}>Reset</button>
    </>
  );
}

useRef, useMemo, useCallback — Tối ưu

useRef tạo một đối tượng có thể thay đổi tồn tại giữa các lần render. Thay đổi current không kích hoạt re-render. Công dụng chính là truy cập phần tử DOM và lưu trữ các giá trị có thể thay đổi (timer, giá trị trước đó, instance thư viện).

useMemo ghi nhớ kết quả của một hàm. Nó chỉ tính toán lại giá trị khi một phụ thuộc thay đổi. Sử dụng nó cho các tính toán tốn kém — lọc mảng lớn, sắp xếp, định dạng dữ liệu. useCallback là một trường hợp đặc biệt của useMemo để ghi nhớ hàm.

js
import React, { useRef, useMemo, useCallback } from 'react';

function SearchPage({ items, query, onItemSelect }) {
  const inputRef = useRef(null);

  // Tự động lấy nét khi mount
  useEffect(() => {
    inputRef.current?.focus();
  }, []);

  // Ghi nhớ kết quả đã lọc
  const filtered = useMemo(() => {
    return items.filter(item =>
      item.name.toLowerCase()
        .includes(query.toLowerCase())
    );
  }, [items, query]);

  // Ghi nhớ callback
  const handleSelect = useCallback((id) => {
    onItemSelect(id);
  }, [onItemSelect]);

  return (
    <div>
      <input ref={inputRef}
        value={query}
        onChange={e => setQuery(e.target.value)}
      />
      <ul>
        {filtered.map(item =>
          <li key={item.id}>
            <button
              onClick={() => handleSelect(item.id)}>
              {item.name}
            </button>
          </li>
        )}
      </ul>
    </div>
  );
}

Một quy tắc quan trọng: useRef, useMemo và useCallback là công cụ tối ưu, không phải cấu trúc bắt buộc. Đừng bọc mọi thứ trong useMemo — hãy đo hiệu suất trước. Tối ưu sớm làm phức tạp mã. React render component nhanh; useMemo chỉ cần thiết khi các tính toán thực sự tốn kém (O(n²) trở lên).

Quy tắc hook và lỗi thường gặp

React đã giới thiệu hai quy tắc nghiêm ngặt cho hook. Thứ nhất: chỉ gọi hook ở cấp cao nhất — không gọi trong điều kiện, vòng lặp hoặc hàm lồng nhau. Thứ hai: chỉ gọi hook từ component hàm React hoặc hook tùy chỉnh. Plugin ESLint eslint-plugin-react-hooks tự động kiểm tra cả hai quy tắc.

Lỗi phổ biến nhất là vi phạm thứ tự gọi hook. React dựa vào thứ tự gọi hook giữa các lần render. Nếu một hook nằm trong điều kiện và không chạy ở một lần render, tất cả các hook tiếp theo sẽ bị dịch chuyển, gây ra lỗi.

LỗiChuyện gì xảy raGiải pháp
Hook trong điều kiệnThứ tự hook bị phá vỡDi chuyển logic điều kiện vào trong hook
Stale closureEffect sử dụng giá trị cũThêm tất cả phụ thuộc vào mảng
Vòng lặp vô hạnuseEffect không phụ thuộc thay đổi stateChỉ định phụ thuộc hoặc dùng useReducer
Thay đổi ref trong renderSide effect trong thân hàmDi chuyển mutation vào useEffect

Lỗi phổ biến thứ hai là stale closure trong useEffect. Nếu effect sử dụng một biến không được chỉ định trong phụ thuộc, effect sẽ “bắt giữ” giá trị của nó tại thời điểm tạo closure. Giải pháp là luôn bao gồm tất cả các biến được sử dụng trong mảng phụ thuộc. Nếu có quá nhiều phụ thuộc, hãy cân nhắc chia effect thành nhiều phần.

Câu hỏi thường gặp

Tại sao không thể gọi hook trong điều kiện?

React dựa vào thứ tự gọi hook giữa các lần render. Nếu một hook được gọi trong điều kiện và không chạy ở một lần render, tất cả các hook tiếp theo bị dịch chuyển — state của chúng bị xáo trộn. Quy tắc ESLint hooks/exhaustive-deps cảnh báo về điều này.

Sự khác biệt giữa useEffect và useLayoutEffect?

useLayoutEffect đồng bộ và chạy trước khi trình duyệt vẽ các thay đổi. useEffect bất đồng bộ — sau khi vẽ. Chỉ sử dụng useLayoutEffect khi bạn cần đo hoặc thay đổi DOM trước khi người dùng thấy thay đổi (vị trí popover, khôi phục cuộn).

Làm thế nào để hủy yêu cầu fetch khi unmount?

Sử dụng AbortController trong useEffect. Tạo một controller, truyền signal vào fetch, và gọi controller.abort() trong hàm dọn dẹp. Đối với trình duyệt cũ, sử dụng cờ cancelled: return () => { cancelled = true; } với kiểm tra trước setState.

Hook tùy chỉnh là gì và khi nào tạo nó?

Hook tùy chỉnh là một hàm JavaScript với tiền tố use sử dụng các hook tích hợp sẵn. Tạo nó khi cùng một logic với hook lặp lại trong hai hoặc nhiều component. Ví dụ: useDebounce, useLocalStorage, useMediaQuery. Hook tùy chỉnh thay thế HOC và render props.

Tại sao useEffect chạy hai lần trong Strict Mode?

Trong Strict Mode, React cố ý mount và unmount component hai lần trong môi trường phát triển để tìm lỗi với các effect không được dọn dẹp. Nếu hàm dọn dẹp được viết đúng, việc gọi hai lần không gây ra vấn đề. Điều này không xảy ra trong chế độ production.

Tổng kết

  • React Hooks — hàm sử dụng state, effect và context trong component hàm, thay thế class.
  • useState — hook cơ bản cho state cục bộ với setter dạng hàm để cập nhật an toàn.
  • useEffect — hook phổ quát cho side effect với mảng phụ thuộc và hàm dọn dẹp.
  • useContext — truy cập trực tiếp context React không cần Consumer wrapper và lồng ghép thừa.
  • useReducer — cho logic state phức tạp với các loại action, thay thế Redux trong một component.
  • useRef, useMemo, useCallback — công cụ tối ưu: tham chiếu DOM, ghi nhớ giá trị và callback.
  • Quy tắc cấp cao nhất — hạn chế nghiêm ngặt việc gọi hook ngoài điều kiện, vòng lặp và hàm lồng nhau.

Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay

IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.

Thảo luận dự án

Đọc thêm