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 Cơ bản

useState

Hook để quản lý state trong functional component.

import { useState } from 'react';

// State đơn giản
function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+</button>
      <button onClick={() => setCount(count - 1)}>-</button>
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  );
}

// State là object
function UserForm() {
  const [user, setUser] = useState({
    name: '',
    email: '',
    age: 0,
  });

  // Cập nhật một field - phải spread toàn bộ object
  function handleChange(field, value) {
    setUser((prev) => ({ ...prev, [field]: value }));
  }

  return (
    <form>
      <input
        value={user.name}
        onChange={(e) => handleChange('name', e.target.value)}
      />
      <input
        value={user.email}
        onChange={(e) => handleChange('email', e.target.value)}
      />
    </form>
  );
}

// Functional update - khi state mới phụ thuộc vào state cũ
function SafeCounter() {
  const [count, setCount] = useState(0);

  // ✅ Dùng functional update để tránh stale state
  function increment() {
    setCount((prev) => prev + 1);
  }

  // ❌ Có thể bị stale trong async context
  function badIncrement() {
    setCount(count + 1);
  }

  return <button onClick={increment}>Count: {count}</button>;
}

// Lazy initial state - tránh tính toán nặng mỗi lần render
function ExpensiveComponent() {
  // ✅ Hàm chỉ chạy một lần khi khởi tạo
  const [data, setData] = useState(() => computeExpensiveValue());

  // ❌ Tính toán mỗi lần render
  const [data2, setData2] = useState(computeExpensiveValue());

  return <div>{data}</div>;
}

useEffect

Hook để thực hiện side effects (fetch data, subscribe, timers, v.v.)

import { useState, useEffect } from 'react';

// Chạy sau mỗi lần render
useEffect(() => {
  console.log('Component rendered');
});

// Chạy một lần sau khi mount
useEffect(() => {
  console.log('Component mounted');
}, []);

// Chạy khi dependency thay đổi
useEffect(() => {
  console.log('userId changed:', userId);
}, [userId]);

// Cleanup function
useEffect(() => {
  const timer = setInterval(() => {
    setTime(new Date());
  }, 1000);

  // Cleanup: chạy trước khi effect chạy lại hoặc khi unmount
  return () => {
    clearInterval(timer);
  };
}, []);

// Fetch data pattern
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let isMounted = true; // Tránh update state sau khi unmount

    async function fetchUser() {
      try {
        setLoading(true);
        const response = await fetch(`/api/users/${userId}`);
        if (!response.ok) throw new Error('Failed to fetch');
        const data = await response.json();

        if (isMounted) {
          setUser(data);
        }
      } catch (err) {
        if (isMounted) {
          setError(err.message);
        }
      } finally {
        if (isMounted) {
          setLoading(false);
        }
      }
    }

    fetchUser();

    return () => {
      isMounted = false;
    };
  }, [userId]);

  if (loading) return <Spinner />;
  if (error) return <ErrorMessage message={error} />;
  return <div>{user?.name}</div>;
}

// Event listener
useEffect(() => {
  function handleResize() {
    setWindowWidth(window.innerWidth);
  }

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

// WebSocket subscription
useEffect(() => {
  const ws = new WebSocket('ws://localhost:8080');
  
  ws.onmessage = (event) => {
    setMessages((prev) => [...prev, event.data]);
  };

  return () => ws.close();
}, []);

Dependency Array - Những lỗi thường gặp

// ❌ Missing dependency
function BadComponent({ value }) {
  useEffect(() => {
    console.log(value); // value không ở trong dependency array
  }, []);
}

// ✅ Đúng
function GoodComponent({ value }) {
  useEffect(() => {
    console.log(value);
  }, [value]);
}

// Object/Array dependency - gây infinite loop
// ❌ Object mới mỗi lần render
function BadListComponent() {
  const options = { page: 1, size: 10 }; // Mới mỗi render

  useEffect(() => {
    fetchData(options);
  }, [options]); // Chạy mỗi render!
}

// ✅ Dùng primitive values
function GoodListComponent() {
  const page = 1;
  const size = 10;

  useEffect(() => {
    fetchData({ page, size });
  }, [page, size]);
}

useRef

Hook để lưu giá trị có thể thay đổi mà không gây re-render, hoặc truy cập DOM elements.

import { useRef, useEffect } from 'react';

// 1. Truy cập DOM element
function FocusInput() {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current?.focus();
  }

  return (
    <>
      <input ref={inputRef} placeholder="Click button to focus" />
      <button onClick={handleClick}>Focus Input</button>
    </>
  );
}

// 2. Lưu giá trị không gây re-render
function Timer() {
  const [seconds, setSeconds] = useState(0);
  const intervalRef = useRef(null);

  function startTimer() {
    intervalRef.current = setInterval(() => {
      setSeconds((prev) => prev + 1);
    }, 1000);
  }

  function stopTimer() {
    clearInterval(intervalRef.current);
  }

  useEffect(() => {
    return () => clearInterval(intervalRef.current);
  }, []);

  return (
    <div>
      <p>{seconds}s</p>
      <button onClick={startTimer}>Start</button>
      <button onClick={stopTimer}>Stop</button>
    </div>
  );
}

// 3. Lưu previous value
function usePrevious(value) {
  const prevRef = useRef(undefined);

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

  return prevRef.current;
}

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

  return (
    <div>
      <p>Current: {count}, Previous: {prevCount}</p>
      <button onClick={() => setCount((c) => c + 1)}>+</button>
    </div>
  );
}

// 4. forwardRef - truyền ref xuống child component
const TextInput = forwardRef(function TextInput({ label, ...props }, ref) {
  return (
    <div>
      <label>{label}</label>
      <input ref={ref} {...props} />
    </div>
  );
});

// Sử dụng
function App() {
  const inputRef = useRef(null);

  return (
    <>
      <TextInput ref={inputRef} label="Name" />
      <button onClick={() => inputRef.current?.focus()}>Focus</button>
    </>
  );
}

useId

Hook để generate unique IDs, hữu ích cho accessibility và server-side rendering.

import { useId } from 'react';

function FormField({ label, type = 'text' }) {
  const id = useId();

  return (
    <div>
      <label htmlFor={id}>{label}</label>
      <input id={id} type={type} />
    </div>
  );
}

// Multiple IDs từ một component
function PasswordField({ label }) {
  const id = useId();
  const descriptionId = `${id}-description`;

  return (
    <div>
      <label htmlFor={id}>{label}</label>
      <input
        id={id}
        type="password"
        aria-describedby={descriptionId}
      />
      <p id={descriptionId}>Password must be at least 8 characters</p>
    </div>
  );
}

So sánh useState vs useRef

useStateuseRef
Gây re-render✅ Có❌ Không
Lưu qua renders✅ Có✅ Có
Tương tựStateInstance variable
Dùng khiCần UI cập nhậtKhông cần UI cập nhật
Ví dụForm valuesTimer ID, DOM refs