Chuyển đến nội dung chính

レッスン 3: フックの詳細

useState、useEffect、useRef、useMemo、useCallback。カスタムフック、フックのルール。複雑な状態には Reducer を使用します。 React 19 use() フック、useTransition、useDeferredValue。

💻 プログラミング — レッスン 3 レッスン 3: フックの詳細

React と Next.js: 基本から高度まで

パート 1: React の基礎

xdev.asia

1. useState

import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  const [user, setUser] = useState<User | null>(null);

  // ⚠️ State updates are asynchronous và batched
  const handleClick = () => {
    setCount(count + 1);     // ❌ Stale closure nếu multiple calls
    setCount(prev => prev + 1); // ✅ Functional update
  };

  // Object state
  const [form, setForm] = useState({ name: '', email: '' });
  const updateName = (name: string) => {
    setForm(prev => ({ ...prev, name })); // ✅ Spread previous state
  };

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

// Lazy initialization (expensive computation)
const [data, setData] = useState(() => {
  return expensiveComputation(); // Chỉ chạy lần đầu
});

2.エフェクトの使用

import { useEffect, useState } from 'react';

function UserProfile({ userId }: { userId: string }) {
  const [user, setUser] = useState<User | null>(null);

  useEffect(() => {
    // Side effect: fetch data
    let cancelled = false;

    async function fetchUser() {
      const res = await fetch(`/api/users/${userId}`);
      const data = await res.json();
      if (!cancelled) {
        setUser(data);
      }
    }

    fetchUser();

    // Cleanup function
    return () => {
      cancelled = true; // Prevent state update after unmount
    };
  }, [userId]); // Dependency array: re-run when userId changes

  return user ? <div>{user.name}</div> : <div>Loading...</div>;
}

// useEffect dependency patterns:
// useEffect(() => {}, []);      // Mount only (componentDidMount)
// useEffect(() => {});          // Every render (⚠️ avoid)
// useEffect(() => {}, [dep]);   // When dep changes
// useEffect(() => { return () => {} }, []); // Unmount cleanup

3. useRef

import { useRef, useEffect } from 'react';

function TextInput() {
  const inputRef = useRef<HTMLInputElement>(null);
  const renderCount = useRef(0); // Mutable value, no re-render

  useEffect(() => {
    inputRef.current?.focus(); // Auto-focus on mount
  }, []);

  useEffect(() => {
    renderCount.current += 1; // Track renders without causing re-render
  });

  return (
    <div>
      <input ref={inputRef} type="text" />
      <p>Rendered {renderCount.current} times</p>
    </div>
  );
}

4. useMemo と useCallback

import { useMemo, useCallback, useState } from 'react';

function ProductList({ products, category }: Props) {
  // useMemo: cache kết quả computation
  const filteredProducts = useMemo(() => {
    return products.filter(p => p.category === category);
  }, [products, category]); // Chỉ re-compute khi dependencies thay đổi

  // useCallback: cache function reference
  const handleDelete = useCallback((id: number) => {
    // delete logic
  }, []); // Stable reference, không tạo function mới mỗi render

  return (
    <div>
      {filteredProducts.map(product => (
        <ProductCard
          key={product.id}
          product={product}
          onDelete={handleDelete} // Stable reference → prevent re-render
        />
      ))}
    </div>
  );
}

// ⚠️ Khi nào dùng useMemo/useCallback:
// ✅ Expensive computations
// ✅ Passing callbacks to memoized children (React.memo)
// ✅ Dependencies of other hooks
// ❌ KHÔNG dùng cho mọi thứ — premature optimization

5.Reducerを使用する

import { useReducer } from 'react';

interface State {
  items: CartItem[];
  total: number;
  isLoading: boolean;
}

type Action =
  | { type: 'ADD_ITEM'; payload: CartItem }
  | { type: 'REMOVE_ITEM'; payload: { id: number } }
  | { type: 'SET_LOADING'; payload: boolean }
  | { type: 'CLEAR_CART' };

function cartReducer(state: State, action: Action): State {
  switch (action.type) {
    case 'ADD_ITEM':
      return {
        ...state,
        items: [...state.items, action.payload],
        total: state.total + action.payload.price,
      };
    case 'REMOVE_ITEM':
      const item = state.items.find(i => i.id === action.payload.id);
      return {
        ...state,
        items: state.items.filter(i => i.id !== action.payload.id),
        total: state.total - (item?.price || 0),
      };
    case 'SET_LOADING':
      return { ...state, isLoading: action.payload };
    case 'CLEAR_CART':
      return { items: [], total: 0, isLoading: false };
    default:
      return state;
  }
}

function Cart() {
  const [state, dispatch] = useReducer(cartReducer, {
    items: [],
    total: 0,
    isLoading: false,
  });

  return (
    <div>
      <p>Total: ${state.total}</p>
      <button onClick={() => dispatch({ type: 'CLEAR_CART' })}>
        Clear Cart
      </button>
    </div>
  );
}

6. カスタムフック

// useLocalStorage
function useLocalStorage<T>(key: string, initialValue: T) {
  const [value, setValue] = useState<T>(() => {
    try {
      const item = localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch {
      return initialValue;
    }
  });

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue] as const;
}

// useDebounce
function useDebounce<T>(value: T, delay: number): T {
  const [debounced, setDebounced] = useState(value);

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

  return debounced;
}

// useFetch
function useFetch<T>(url: string) {
  const [data, setData] = useState<T | null>(null);
  const [error, setError] = useState<Error | null>(null);
  const [isLoading, setIsLoading] = useState(true);

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

    async function fetchData() {
      try {
        setIsLoading(true);
        const res = await fetch(url);
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        const json = await res.json();
        if (!cancelled) setData(json);
      } catch (err) {
        if (!cancelled) setError(err as Error);
      } finally {
        if (!cancelled) setIsLoading(false);
      }
    }

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

  return { data, error, isLoading };
}

// Usage
function Users() {
  const { data: users, error, isLoading } = useFetch<User[]>('/api/users');
  const [search, setSearch] = useState('');
  const debouncedSearch = useDebounce(search, 300);

  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;

  return <div>{/* render users */}</div>;
}

7. React 19 フック

import { useTransition, useDeferredValue, use } from 'react';

// useTransition: mark state update as non-urgent
function Search() {
  const [query, setQuery] = useState('');
  const [isPending, startTransition] = useTransition();

  const handleChange = (value: string) => {
    setQuery(value); // Urgent: update input immediately

    startTransition(() => {
      // Non-urgent: update filtered list (can be interrupted)
      setFilteredItems(filterItems(value));
    });
  };

  return (
    <div>
      <input value={query} onChange={e => handleChange(e.target.value)} />
      {isPending && <Spinner />}
    </div>
  );
}

// useDeferredValue: defer re-rendering of a value
function SearchResults({ query }: { query: string }) {
  const deferredQuery = useDeferredValue(query);
  const isStale = query !== deferredQuery;

  return (
    <div style={{ opacity: isStale ? 0.5 : 1 }}>
      <Results query={deferredQuery} />
    </div>
  );
}

次の記事: イベント処理、フォーム、条件付きレンダリング — 制御されたフォーム、React Hook フォーム、Zod 検証。