Mục lục

Deep Dive: React Hooks Under The Hood

Hiểu sâu về cơ chế hoạt động của useState, useEffect, useMemo, useRef. Tại sao hook lại hoạt động như vậy? Khi nào dùng và những sai lầm chết người cần tránh.

React Hooks không phải là ma thuật. Chúng là các hàm Javascript thông thường dựa vào Closure và một danh sách liên kết (Linked List) được lưu trong React Fiber.

Hãy cùng mổ xẻ từng hook phổ biến.


1. useState: Ký ức của Component

Tại sao nó tồn tại?

Trong JS thuần, biến local let count = 0 sẽ bị reset mỗi khi hàm chạy lại. React Components là hàm, nên nó cần một "kho chứa" bên ngoài để ghi nhớ giá trị count giữa các lần render. kho chứa đó nằm trong Fiber Node.

Cơ chế hoạt động (Simplified)

javascript:
// Mô phỏng useState bằng Array (Fiber Hooks List)
let hooks = []; // Kho chứa
let cursor = 0; // Con trỏ đọc/ghi

function useState(initialValue) {
  const currentCursor = cursor; // Nhớ vị trí của slot này
  hooks[currentCursor] = hooks[currentCursor] || initialValue; // Init nếu chưa có

  const setState = (newValue) => {
    hooks[currentCursor] = newValue;
    render(); // Kích hoạt render lại
  };

  cursor++; // Tiến tới slot tiếp cho hook sau
  return [hooks[currentCursor], setState];
}

1.3. Tại sao thứ tự gọi Hook lại quan trọng?

Vì React lưu hooks trong một Linked List (hoặc Array trong ví dụ trên), nó dựa vào thứ tự gọi để biết useState nào thuộc về biến nào.

text:
Lần render 1:
1. useState('A') -> Slot[0]
2. useState('B') -> Slot[1]
3. useEffect()   -> Slot[2]

Lần render 2 (Nếu bạn dùng if để ẩn Hook 2):
1. useState('A') -> Slot[0] (OK)
2. useEffect()   -> Slot[1] (😱 Lỗi! React tưởng đây là useState('B'))

👉 Hậu quả: React sẽ lấy state của hook này lắp vào hook kia, gây lỗi "Rendered fewer hooks than expected" hoặc sai dữ liệu nghiêm trọng. Đó là lý do Rule of Hooks cấm gọi hook trong if/for.


❌ Cách dùng sai (Anti-pattern)

1. State dư thừa (Redundant State) Lưu cái có thể tính toán được vào state.

tsx:
// ❌ Bad
const [firstName] = useState('John');
const [lastName] = useState('Doe');
const [fullName, setFullName] = useState('John Doe'); // Dư thừa, dễ sync sai

// ✅ Good
const fullName = `${firstName} ${lastName}`; // Tính toán ngay trong render (Derived State)

2. Update state dựa trên biến cũ mà không dùng callback

tsx:
// ❌ Bad: Nếu gọi 2 lần liên tiếp, kết quả có thể sai do closure cũ
setCount(count + 1);
setCount(count + 1); // Kết quả chỉ tăng 1

// ✅ Good
setCount(prev => prev + 1);
setCount(prev => prev + 1); // Kết quả tăng 2

2. useEffect: Đồng bộ hóa với Bên ngoài

Tại sao nó tồn tại?

React là Pure Function (Input -> Output). Nhưng app thực tế cần: gọi API, lắng nghe WebSocket, tương tác DOM thật... Đây là Side Effects. useEffect là nơi an toàn để thực hiện chúng sau khi render xong.

Mental Model (Mô hình tư duy)

useEffect không phải là Lifecycle (componentDidMount). Hãy nghĩ nó là: "Đồng bộ hóa (Synchronize) hệ thống với một biến dependency".

⚠️ Lưu ý chết người: Dependency Array

  • Danh sách [] rỗng: Chỉ chạy 1 lần (Mount).
  • Không truyền []: Chạy mỗi lần render (Dễ infinite loop).
  • [prop, input]: Chạy khi prop hoặc input thay đổi (chỉ so sánh nông ===).

❌ Common Pitfall: Object/Array Dependency

tsx:
// ❌ Bad: Infinite Loop tiềm ẩn
const options = { id: 1 }; // Object mới được tạo ra mỗi lần render (địa chỉ RAM khác nhau)
useEffect(() => {
  fetchData(options);
}, [options]); // Effect chạy lại liên tục -> Loop!

// ✅ Fix: Dùng useMemo hoặc chuyển object ra ngoài component
const options = useMemo(() => ({ id: 1 }), []);

3. useMemo & useCallback: Performance Tuning

Bản chất

Cả hai đều dùng kỹ thuật Memoization (Ghi nhớ kết quả).

  • useMemo: Ghi nhớ kết quả trả về của hàm (Giá trị).
  • useCallback: Ghi nhớ chính cái hàm đó (Tham chiếu).

Khi nào dùng? (Không phải lúc nào cũng dùng!)

✅ Dùng khi:

  1. Tính toán rất nặng: Filter/Sort array 10,000 items. useMemo.
  2. Referential Equality: Bạn truyền một object/function làm prop xuống component con, và component con được bọc React.memo. Nếu không dùng useMemo/useCallback, component con sẽ re-render vô nghĩa vì prop thay đổi tham chiếu.

❌ Đừng dùng khi: Tính toán đơn giản (a + b, filter array 10 items). Chi phí tạo bộ nhớ cache còn đắt hơn tính lại!

tsx:
// ❌ Premature Optimization
const value = useMemo(() => item.price * 2, [item.price]); // Quá thừa thãi

4. useRef: Chiếc hộp ma thuật (Escape Hatch)

Tác dụng kép

  1. Truy cập DOM thật: Focus input, đo chiều rộng div.
  2. Biến Mutable không gây Re-render: Lưu giá trị mà bạn muốn thay đổi âm thầm (timer ID, previous props) mà không muốn component vẽ lại.

So sánh với useState

useStateuseRef
Thay đổi giá trịsetVal(newVal)ref.current = newVal
Re-render?✅ CÓ (Trigger render)❌ KHÔNG (Im lặng)
Khi nào dùng?Dữ liệu hiển thị lên màn hìnhTimer, DOM, biến tạm logic

❌ Sai lầm: Đọc/Ghi Ref trong Render

tsx:
// ❌ Tuyệt đối cấm
function Component() {
  const count = useRef(0);
  
  count.current++; // ⚠️ Side effect trong render! Gây bug khó hiểu khi React chạy srict mode hoặc concurrent features.
  
  return <div>{count.current}</div>; // Dữ liệu hiển thị không nên dùng Ref
}

5. useReducer: Trùm cuối quản lý State

Khi nào dùng thay useState?

  1. State phức tạp: State là object nhiều tầng, mảng lồng nhau.
  2. State phụ thuộc lẫn nhau: Chỉnh status thì phải reset error, xóa data.
  3. Business Logic dày: Muốn tách biệt logic update ra khỏi UI (như bài State Machine Flow).

(Xem chi tiết bài: Complex Flows với State Machine)


Tóm tắt: "Rule of hooks"

  1. Chỉ gọi ở Top Level: Không gọi trong if, for, while. Vì React dựa vào thứ tự gọi để biết hook nào là hook nào.
  2. Chỉ gọi trong React Component/Custom Hook: Không gọi trong hàm JS thường.
Quảng cáo
mdhorizontal