Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Hooks Nâng cao

useReducer

Thích hợp khi state logic phức tạp với nhiều sub-values hoặc state tiếp theo phụ thuộc vào state cũ.

import { useReducer } from 'react';

// Định nghĩa reducer
function cartReducer(state, action) {
  switch (action.type) {
    case 'ADD_ITEM': {
      const existingItem = state.items.find((i) => i.id === action.payload.id);
      if (existingItem) {
        return {
          ...state,
          items: state.items.map((i) =>
            i.id === action.payload.id
              ? { ...i, quantity: i.quantity + 1 }
              : i
          ),
        };
      }
      return {
        ...state,
        items: [...state.items, { ...action.payload, quantity: 1 }],
      };
    }

    case 'REMOVE_ITEM':
      return {
        ...state,
        items: state.items.filter((i) => i.id !== action.payload.id),
      };

    case 'CLEAR_CART':
      return { ...state, items: [] };

    case 'UPDATE_QUANTITY':
      return {
        ...state,
        items: state.items.map((i) =>
          i.id === action.payload.id
            ? { ...i, quantity: action.payload.quantity }
            : i
        ),
      };

    default:
      return state;
  }
}

const initialState = { items: [], discount: 0 };

function ShoppingCart() {
  const [cart, dispatch] = useReducer(cartReducer, initialState);

  const total = cart.items.reduce(
    (sum, item) => sum + item.price * item.quantity,
    0
  );

  return (
    <div>
      <h2>Cart ({cart.items.length} items)</h2>
      {cart.items.map((item) => (
        <div key={item.id}>
          <span>{item.name} x{item.quantity}</span>
          <button onClick={() => dispatch({ type: 'REMOVE_ITEM', payload: { id: item.id } })}>
            Remove
          </button>
        </div>
      ))}
      <p>Total: ${total.toFixed(2)}</p>
      <button onClick={() => dispatch({ type: 'CLEAR_CART' })}>
        Clear Cart
      </button>
    </div>
  );
}

useState vs useReducer

Dùng useState khi:            Dùng useReducer khi:
- State đơn giản              - State là object phức tạp
- Ít logic cập nhật           - Nhiều actions khác nhau
- Không phụ thuộc vào nhau    - Logic cập nhật phức tạp
- Ví dụ: boolean, string      - Ví dụ: shopping cart, form

useMemo

Memoize giá trị tính toán tốn kém, chỉ tính lại khi dependencies thay đổi.

import { useMemo, useState } from 'react';

// ✅ Dùng useMemo khi tính toán nặng
function ProductList({ products, filters }) {
  const filteredProducts = useMemo(() => {
    return products
      .filter((p) => {
        if (filters.category && p.category !== filters.category) return false;
        if (filters.minPrice && p.price < filters.minPrice) return false;
        if (filters.maxPrice && p.price > filters.maxPrice) return false;
        return true;
      })
      .sort((a, b) => {
        if (filters.sortBy === 'price') return a.price - b.price;
        if (filters.sortBy === 'name') return a.name.localeCompare(b.name);
        return 0;
      });
  }, [products, filters]); // Chỉ tính lại khi products hoặc filters thay đổi

  return (
    <ul>
      {filteredProducts.map((p) => (
        <li key={p.id}>{p.name} - ${p.price}</li>
      ))}
    </ul>
  );
}

// ✅ Memoize reference để dùng trong useEffect dependency
function DataComponent({ config }) {
  const processedConfig = useMemo(() => ({
    apiUrl: config.baseUrl + '/api',
    timeout: config.timeout ?? 5000,
  }), [config.baseUrl, config.timeout]);

  useEffect(() => {
    fetchData(processedConfig);
  }, [processedConfig]); // processedConfig stable khi config không đổi
}

// ❌ Không cần useMemo cho tính toán đơn giản
const doubled = useMemo(() => count * 2, [count]); // Quá mức cần thiết
const doubled2 = count * 2; // Đủ rồi

useCallback

Memoize function reference, hữu ích khi truyền callback vào child component được memo hóa.

import { useCallback, memo } from 'react';

// Vấn đề: Function mới mỗi render gây re-render không cần thiết
function ParentBad() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');

  // ❌ Function mới mỗi lần parent render
  const handleDelete = (id) => {
    setItems((prev) => prev.filter((i) => i.id !== id));
  };

  return (
    <>
      <input value={name} onChange={(e) => setName(e.target.value)} />
      {/* ItemList re-render mỗi khi name thay đổi do handleDelete mới */}
      <ItemList onDelete={handleDelete} />
    </>
  );
}

// ✅ Giải pháp với useCallback
function ParentGood() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');

  const handleDelete = useCallback((id) => {
    setItems((prev) => prev.filter((i) => i.id !== id));
  }, []); // Empty deps vì dùng functional update

  return (
    <>
      <input value={name} onChange={(e) => setName(e.target.value)} />
      {/* ItemList KHÔNG re-render khi name thay đổi */}
      <MemoizedItemList onDelete={handleDelete} />
    </>
  );
}

// Child component được memo hóa
const MemoizedItemList = memo(function ItemList({ items, onDelete }) {
  console.log('ItemList rendered');
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>
          {item.name}
          <button onClick={() => onDelete(item.id)}>Delete</button>
        </li>
      ))}
    </ul>
  );
});

useLayoutEffect

Giống useEffect nhưng chạy đồng bộ sau khi DOM mutation, trước khi browser paint.

import { useLayoutEffect, useRef } from 'react';

// Dùng khi cần đọc layout từ DOM trước khi paint
function Tooltip({ text, targetRef }) {
  const tooltipRef = useRef(null);

  useLayoutEffect(() => {
    const tooltip = tooltipRef.current;
    const target = targetRef.current;

    if (!tooltip || !target) return;

    // Đọc vị trí DOM - cần đồng bộ để tránh flicker
    const targetRect = target.getBoundingClientRect();
    tooltip.style.top = `${targetRect.bottom + 8}px`;
    tooltip.style.left = `${targetRect.left}px`;
  });

  return (
    <div ref={tooltipRef} className="tooltip" role="tooltip">
      {text}
    </div>
  );
}
useEffect vs useLayoutEffect:

Browser: Render DOM  →  useLayoutEffect  →  Paint  →  useEffect
         (commit)         (sync)             (screen)   (async)

Dùng useLayoutEffect khi:
- Cần đọc/ghi DOM trước khi paint
- Animation, tooltip positioning
- Tránh visual flash

Dùng useEffect (thường) khi:
- Fetch data
- Event subscriptions
- Logging

Custom Hooks

Tái sử dụng stateful logic giữa các components.

// useLocalStorage - lưu state vào localStorage
function useLocalStorage(key, initialValue) {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch {
      return initialValue;
    }
  });

  const setValue = useCallback((value) => {
    try {
      const valueToStore =
        value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.error('Error saving to localStorage:', error);
    }
  }, [key, storedValue]);

  return [storedValue, setValue];
}

// Sử dụng
function Settings() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  return (
    <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
      Toggle theme: {theme}
    </button>
  );
}

// useFetch - data fetching
function useFetch(url) {
  const [state, setState] = useState({
    data: null,
    loading: true,
    error: null,
  });

  useEffect(() => {
    let cancelled = false;

    setState({ data: null, loading: true, error: null });

    fetch(url)
      .then((res) => {
        if (!res.ok) throw new Error(`HTTP error ${res.status}`);
        return res.json();
      })
      .then((data) => {
        if (!cancelled) setState({ data, loading: false, error: null });
      })
      .catch((error) => {
        if (!cancelled) setState({ data: null, loading: false, error });
      });

    return () => { cancelled = true; };
  }, [url]);

  return state;
}

// useDebounce - delay value update
function useDebounce(value, delay = 300) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);

  return debouncedValue;
}

// Sử dụng useDebounce cho search
function SearchInput() {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDebounce(query, 300);

  const { data } = useFetch(
    debouncedQuery ? `/api/search?q=${debouncedQuery}` : null
  );

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search..."
      />
      <SearchResults results={data} />
    </div>
  );
}

// useToggle - boolean toggle
function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue);
  const toggle = useCallback(() => setValue((v) => !v), []);
  return [value, toggle];
}

// usePrevious - lưu giá trị trước đó
function usePrevious(value) {
  const ref = useRef(undefined);

  useEffect(() => {
    ref.current = value;
  });

  return ref.current;
}

// useWindowSize - reactive window dimensions
function useWindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight,
  });

  useEffect(() => {
    const handleResize = () =>
      setSize({ width: window.innerWidth, height: window.innerHeight });

    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return size;
}

Rules of Hooks

// ✅ Gọi ở top level của component
function GoodComponent() {
  const [count, setCount] = useState(0); // OK
  const data = useFetch('/api/data');     // OK
}

// ❌ Không gọi trong điều kiện
function BadComponent({ show }) {
  if (show) {
    const [count] = useState(0); // LỖI! Không được trong if
  }
}

// ❌ Không gọi trong loop
function BadLoop() {
  for (let i = 0; i < 3; i++) {
    const [val] = useState(i); // LỖI!
  }
}

// ❌ Không gọi trong nested function thường
function BadNested() {
  function setupSomething() {
    const [val] = useState(0); // LỖI!
  }
}

// ✅ Được gọi trong custom hook
function useMyHook() {
  const [val] = useState(0); // OK - custom hook
  return val;
}