Skip to content

📅 NGÀY 29: Custom Hooks với useReducer - Reusable Logic Patterns

📍 Vị trí: Phase 3, Tuần 6, Ngày 29/45

⏱️ Thời lượng: 3-4 giờ


🎯 Mục tiêu học tập (5 phút)

Sau bài học này, bạn sẽ:

  • [ ] Tạo được custom hooks encapsulate useReducer logic
  • [ ] Implement được useFetch, useAsync, useForm hooks
  • [ ] Compose được multiple hooks together
  • [ ] Share được logic across components without duplication
  • [ ] Test được custom hooks (conceptual understanding)

🤔 Kiểm tra đầu vào (5 phút)

Trước khi bắt đầu, hãy trả lời 3 câu hỏi sau:

  1. Khi nào nên extract logic thành custom hook?

    • Gợi ý: Code được dùng ở 2+ components, logic phức tạp...
  2. Custom hook khác function thông thường như thế nào?

    • Gợi ý: Naming convention, có thể dùng hooks bên trong...
  3. Bạn đã gặp trường hợp copy-paste logic giữa components chưa?

    • Ví dụ: Fetch user data ở nhiều nơi, form handling duplicate...

📖 PHẦN 1: GIỚI THIỆU KHÁI NIỆM (30 phút)

1.1 Vấn Đề Thực Tế

Hãy tưởng tượng bạn có 3 components đều fetch data từ API:

jsx
// ❌ VẤN ĐỀ: Duplicate Logic Everywhere

// Component 1: UserProfile
function UserProfile({ userId }) {
  const [state, dispatch] = useReducer(dataReducer, initialState);

  useEffect(() => {
    const controller = new AbortController();

    const fetchUser = async () => {
      dispatch({ type: 'FETCH_START' });
      try {
        const res = await fetch(`/api/users/${userId}`, {
          signal: controller.signal,
        });
        const data = await res.json();
        dispatch({ type: 'FETCH_SUCCESS', payload: data });
      } catch (err) {
        if (err.name !== 'AbortError') {
          dispatch({ type: 'FETCH_ERROR', payload: err.message });
        }
      }
    };

    fetchUser();
    return () => controller.abort();
  }, [userId]);

  // ... render
}

// Component 2: PostList
function PostList() {
  const [state, dispatch] = useReducer(dataReducer, initialState);

  useEffect(() => {
    const controller = new AbortController();

    const fetchPosts = async () => {
      dispatch({ type: 'FETCH_START' });
      try {
        const res = await fetch('/api/posts', {
          signal: controller.signal,
        });
        const data = await res.json();
        dispatch({ type: 'FETCH_SUCCESS', payload: data });
      } catch (err) {
        if (err.name !== 'AbortError') {
          dispatch({ type: 'FETCH_ERROR', payload: err.message });
        }
      }
    };

    fetchPosts();
    return () => controller.abort();
  }, []);

  // ... render
}

// Component 3: CommentList
function CommentList({ postId }) {
  const [state, dispatch] = useReducer(dataReducer, initialState);

  useEffect(() => {
    const controller = new AbortController();

    const fetchComments = async () => {
      dispatch({ type: 'FETCH_START' });
      try {
        const res = await fetch(`/api/posts/${postId}/comments`, {
          signal: controller.signal,
        });
        const data = await res.json();
        dispatch({ type: 'FETCH_SUCCESS', payload: data });
      } catch (err) {
        if (err.name !== 'AbortError') {
          dispatch({ type: 'FETCH_ERROR', payload: err.message });
        }
      }
    };

    fetchComments();
    return () => controller.abort();
  }, [postId]);

  // ... render
}

Vấn đề:

  1. 🔴 90% code giống nhau → Copy-paste nightmare
  2. 🔴 Bug fix phải sửa 3 chỗ → Easy to miss
  3. 🔴 Add feature (retry) phải update everywhere → Unmaintainable
  4. 🔴 Hard to test → Test 3 components riêng biệt
  5. 🔴 No single source of truth → Inconsistent behavior

1.2 Giải Pháp: Custom Hook

Custom Hook = Function extract reusable logic

jsx
// ✅ GIẢI PHÁP: useFetch Hook
function useFetch(url) {
  const initialState = {
    data: null,
    loading: false,
    error: null,
  };

  const [state, dispatch] = useReducer(dataReducer, initialState);

  useEffect(() => {
    if (!url) return;

    const controller = new AbortController();

    const fetchData = async () => {
      dispatch({ type: 'FETCH_START' });

      try {
        const res = await fetch(url, { signal: controller.signal });
        const data = await res.json();
        dispatch({ type: 'FETCH_SUCCESS', payload: data });
      } catch (err) {
        if (err.name !== 'AbortError') {
          dispatch({ type: 'FETCH_ERROR', payload: err.message });
        }
      }
    };

    fetchData();
    return () => controller.abort();
  }, [url]);

  return state;
}

// ✅ USAGE: Clean & Consistent
function UserProfile({ userId }) {
  const { data, loading, error } = useFetch(`/api/users/${userId}`);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  return <div>{data?.name}</div>;
}

function PostList() {
  const { data, loading, error } = useFetch('/api/posts');
  // ... render
}

function CommentList({ postId }) {
  const { data, loading, error } = useFetch(`/api/posts/${postId}/comments`);
  // ... render
}

Lợi ích:

  • DRY (Don't Repeat Yourself) → Logic ở 1 chỗ
  • Easy to maintain → Bug fix/feature add 1 lần
  • Testable → Test hook độc lập
  • Reusable → Dùng ở unlimited components
  • Consistent → Same behavior everywhere

1.3 Mental Model

┌───────────────────────────────────────────────┐
│          CUSTOM HOOK (Logic Container)         │
│                                                │
│  function useFetch(url) {                     │
│    const [state, dispatch] = useReducer(...)  │
│    useEffect(() => { ... }, [url])            │
│    return { data, loading, error }            │
│  }                                             │
│                                                │
│  ✅ Encapsulates:                             │
│  • State management (useReducer)              │
│  • Side effects (useEffect)                   │
│  • Cleanup logic                              │
│  • Error handling                             │
└───────────────────────────────────────────────┘
         ↓                ↓                ↓
┌─────────────┐  ┌─────────────┐  ┌─────────────┐
│ Component A │  │ Component B │  │ Component C │
│             │  │             │  │             │
│ useFetch(A) │  │ useFetch(B) │  │ useFetch(C) │
│             │  │             │  │             │
│ Render UI   │  │ Render UI   │  │ Render UI   │
└─────────────┘  └─────────────┘  └─────────────┘

Analogy: Custom Hook giống Recipe (công thức nấu ăn)

  • Recipe: Định nghĩa steps (logic)
  • Cook: Components dùng recipe
  • Ingredients: Parameters (url, userId, etc.)
  • Result: Cooked dish (data, loading, error)

Nhiều cooks có thể dùng cùng recipe → Consistent dishes!

1.4 Hiểu Lầm Phổ Biến

Hiểu lầm 1: "Custom hook chỉ là rename function"

  • Sự thật: Custom hook CÓ THỂ dùng hooks (useState, useEffect, etc.). Regular function KHÔNG THỂ!

Hiểu lầm 2: "Custom hook phải bắt đầu bằng 'use'"

  • Sự thật: ĐÂY LÀ QUY TẮC BẮT BUỘC! React dựa vào naming để check Rules of Hooks

Hiểu lầm 3: "Custom hooks share state giữa components"

  • Sự thật: Mỗi component gọi hook có STATE RIÊNG. Hook chỉ share LOGIC, không share state!

Hiểu lầm 4: "Nên tạo custom hook cho mọi thứ"

  • Sự thật: Chỉ extract khi có reuse hoặc logic phức tạp. Premature abstraction = bad!

💻 PHẦN 2: LIVE CODING (45 phút)

Demo 1: useFetch - Basic Data Fetching Hook ⭐

jsx
import { useReducer, useEffect } from 'react';

// 🏭 REDUCER
function fetchReducer(state, action) {
  switch (action.type) {
    case 'FETCH_START':
      return { data: null, loading: true, error: null };

    case 'FETCH_SUCCESS':
      return { data: action.payload, loading: false, error: null };

    case 'FETCH_ERROR':
      return { data: null, loading: false, error: action.payload };

    default:
      throw new Error(`Unknown action: ${action.type}`);
  }
}

// 🎯 CUSTOM HOOK: useFetch
function useFetch(url, options = {}) {
  const initialState = {
    data: null,
    loading: false,
    error: null,
  };

  const [state, dispatch] = useReducer(fetchReducer, initialState);

  useEffect(() => {
    // ⚠️ Skip nếu no URL
    if (!url) return;

    const controller = new AbortController();

    const fetchData = async () => {
      dispatch({ type: 'FETCH_START' });

      try {
        const response = await fetch(url, {
          ...options,
          signal: controller.signal,
        });

        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }

        const data = await response.json();
        dispatch({ type: 'FETCH_SUCCESS', payload: data });
      } catch (error) {
        if (error.name !== 'AbortError') {
          dispatch({ type: 'FETCH_ERROR', payload: error.message });
        }
      }
    };

    fetchData();

    // Cleanup
    return () => controller.abort();
  }, [url]); // Re-fetch khi URL changes

  return state;
}

// ✅ USAGE EXAMPLES

// Example 1: Simple fetch
function UserList() {
  const { data, loading, error } = useFetch(
    'https://jsonplaceholder.typicode.com/users',
  );

  if (loading) return <div>Loading users...</div>;
  if (error) return <div>Error: {error}</div>;

  return (
    <ul>
      {data?.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

// Example 2: Dynamic URL
function UserProfile({ userId }) {
  const { data, loading, error } = useFetch(
    userId ? `https://jsonplaceholder.typicode.com/users/${userId}` : null,
  );

  if (!userId) return <div>Select a user</div>;
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;

  return (
    <div>
      <h2>{data?.name}</h2>
      <p>{data?.email}</p>
    </div>
  );
}

// Example 3: POST request
function CreatePost() {
  const [title, setTitle] = useState('');
  const [shouldPost, setShouldPost] = useState(false);

  const { data, loading, error } = useFetch(
    shouldPost ? 'https://jsonplaceholder.typicode.com/posts' : null,
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ title, userId: 1 }),
    },
  );

  const handleSubmit = (e) => {
    e.preventDefault();
    setShouldPost(true);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={title}
        onChange={(e) => setTitle(e.target.value)}
      />
      <button
        type='submit'
        disabled={loading}
      >
        {loading ? 'Creating...' : 'Create Post'}
      </button>
      {error && <div>Error: {error}</div>}
      {data && <div>Created post #{data.id}</div>}
    </form>
  );
}

🎯 Key Points:

  1. Hook Naming:

    • MUST start with use (React rule)
    • Descriptive name: useFetch, NOT getData
  2. Parameters:

    • Accept url and options
    • Flexible for different use cases
  3. Return Value:

    • Return state object: { data, loading, error }
    • Destructure in component
  4. Each Component = Separate State:

    • UserList có state riêng
    • UserProfile có state riêng
    • Hook chỉ share logic!

Demo 2: useAsync - Generic Async Hook ⭐⭐

jsx
// 🎯 CUSTOM HOOK: useAsync (More flexible than useFetch)
function useAsync(asyncFunction, immediate = true) {
  const initialState = {
    data: null,
    loading: immediate, // Start loading nếu immediate
    error: null,
  };

  const [state, dispatch] = useReducer(fetchReducer, initialState);

  // ✅ Execute function manually
  const execute = async (...params) => {
    dispatch({ type: 'FETCH_START' });

    try {
      const data = await asyncFunction(...params);
      dispatch({ type: 'FETCH_SUCCESS', payload: data });
      return data;
    } catch (error) {
      dispatch({ type: 'FETCH_ERROR', payload: error.message });
      throw error;
    }
  };

  // ✅ Auto-execute on mount nếu immediate = true
  useEffect(() => {
    if (immediate) {
      execute();
    }
  }, []); // Empty deps = run once

  return { ...state, execute };
}

// ✅ USAGE EXAMPLES

// Example 1: Auto-execute on mount
function UserList() {
  const fetchUsers = async () => {
    const res = await fetch('https://jsonplaceholder.typicode.com/users');
    return res.json();
  };

  const { data, loading, error } = useAsync(fetchUsers, true);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;

  return (
    <ul>
      {data?.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

// Example 2: Manual execution (button click)
function CreateUser() {
  const [name, setName] = useState('');

  const createUser = async (userName) => {
    const res = await fetch('https://jsonplaceholder.typicode.com/users', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ name: userName }),
    });
    return res.json();
  };

  const { data, loading, error, execute } = useAsync(createUser, false);

  const handleSubmit = async (e) => {
    e.preventDefault();
    try {
      await execute(name);
      setName(''); // Clear input on success
    } catch (err) {
      // Error already handled by hook
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
      <button
        type='submit'
        disabled={loading}
      >
        {loading ? 'Creating...' : 'Create'}
      </button>
      {error && <div style={{ color: 'red' }}>{error}</div>}
      {data && <div style={{ color: 'green' }}>Created: {data.name}</div>}
    </form>
  );
}

// Example 3: Retry functionality
function DataWithRetry() {
  const fetchData = async () => {
    // Simulate random failure
    if (Math.random() > 0.5) {
      throw new Error('Random failure');
    }
    const res = await fetch('https://jsonplaceholder.typicode.com/posts/1');
    return res.json();
  };

  const { data, loading, error, execute } = useAsync(fetchData, true);

  return (
    <div>
      {loading && <div>Loading...</div>}
      {error && (
        <div>
          <div style={{ color: 'red' }}>Error: {error}</div>
          <button onClick={execute}>Retry</button>
        </div>
      )}
      {data && (
        <div>
          <h3>{data.title}</h3>
          <p>{data.body}</p>
        </div>
      )}
    </div>
  );
}

🎯 useAsync vs useFetch:

FeatureuseFetchuseAsync
URL-specific✅ Yes❌ No
Any async fn❌ No✅ Yes
Manual trigger❌ Auto✅ execute()
Flexibility⚠️ Medium✅ High

Demo 3: useForm - Form Management Hook ⭐⭐⭐

jsx
// 🏭 FORM REDUCER
function formReducer(state, action) {
  switch (action.type) {
    case 'UPDATE_FIELD':
      return {
        ...state,
        values: {
          ...state.values,
          [action.payload.name]: action.payload.value,
        },
        touched: {
          ...state.touched,
          [action.payload.name]: true,
        },
        // Clear error khi user edits
        errors: {
          ...state.errors,
          [action.payload.name]: undefined,
        },
      };

    case 'SET_ERRORS':
      return {
        ...state,
        errors: action.payload.errors,
      };

    case 'SET_SUBMITTING':
      return {
        ...state,
        isSubmitting: action.payload.isSubmitting,
      };

    case 'RESET_FORM':
      return action.payload.initialState;

    default:
      throw new Error(`Unknown action: ${action.type}`);
  }
}

// 🎯 CUSTOM HOOK: useForm
function useForm(initialValues, validate, onSubmit) {
  const initialState = {
    values: initialValues,
    errors: {},
    touched: {},
    isSubmitting: false,
  };

  const [state, dispatch] = useReducer(formReducer, initialState);

  // ✅ Handle field change
  const handleChange = (e) => {
    const { name, value } = e.target;
    dispatch({
      type: 'UPDATE_FIELD',
      payload: { name, value },
    });
  };

  // ✅ Handle blur (mark as touched)
  const handleBlur = (e) => {
    const { name } = e.target;
    dispatch({
      type: 'UPDATE_FIELD',
      payload: { name, value: state.values[name] },
    });
  };

  // ✅ Handle submit
  const handleSubmit = async (e) => {
    e.preventDefault();

    // Validate
    const validationErrors = validate(state.values);

    if (Object.keys(validationErrors).length > 0) {
      dispatch({
        type: 'SET_ERRORS',
        payload: { errors: validationErrors },
      });
      return;
    }

    // Submit
    dispatch({ type: 'SET_SUBMITTING', payload: { isSubmitting: true } });

    try {
      await onSubmit(state.values);
      // Reset form on success
      dispatch({ type: 'RESET_FORM', payload: { initialState } });
    } catch (error) {
      dispatch({
        type: 'SET_ERRORS',
        payload: { errors: { submit: error.message } },
      });
    } finally {
      dispatch({ type: 'SET_SUBMITTING', payload: { isSubmitting: false } });
    }
  };

  // ✅ Reset form manually
  const reset = () => {
    dispatch({ type: 'RESET_FORM', payload: { initialState } });
  };

  return {
    values: state.values,
    errors: state.errors,
    touched: state.touched,
    isSubmitting: state.isSubmitting,
    handleChange,
    handleBlur,
    handleSubmit,
    reset,
  };
}

// ✅ USAGE EXAMPLE
function RegistrationForm() {
  // Initial values
  const initialValues = {
    username: '',
    email: '',
    password: '',
  };

  // Validation function
  const validate = (values) => {
    const errors = {};

    if (!values.username) {
      errors.username = 'Username is required';
    } else if (values.username.length < 3) {
      errors.username = 'Username must be at least 3 characters';
    }

    if (!values.email) {
      errors.email = 'Email is required';
    } else if (!/\S+@\S+\.\S+/.test(values.email)) {
      errors.email = 'Email is invalid';
    }

    if (!values.password) {
      errors.password = 'Password is required';
    } else if (values.password.length < 6) {
      errors.password = 'Password must be at least 6 characters';
    }

    return errors;
  };

  // Submit handler
  const onSubmit = async (values) => {
    // Simulate API call
    await new Promise((resolve) => setTimeout(resolve, 1000));
    console.log('Form submitted:', values);
    alert('Registration successful!');
  };

  // ✅ Use the hook
  const {
    values,
    errors,
    touched,
    isSubmitting,
    handleChange,
    handleBlur,
    handleSubmit,
    reset,
  } = useForm(initialValues, validate, onSubmit);

  return (
    <form onSubmit={handleSubmit}>
      {/* Username */}
      <div>
        <label>Username:</label>
        <input
          type='text'
          name='username'
          value={values.username}
          onChange={handleChange}
          onBlur={handleBlur}
        />
        {touched.username && errors.username && (
          <span style={{ color: 'red' }}>{errors.username}</span>
        )}
      </div>

      {/* Email */}
      <div>
        <label>Email:</label>
        <input
          type='email'
          name='email'
          value={values.email}
          onChange={handleChange}
          onBlur={handleBlur}
        />
        {touched.email && errors.email && (
          <span style={{ color: 'red' }}>{errors.email}</span>
        )}
      </div>

      {/* Password */}
      <div>
        <label>Password:</label>
        <input
          type='password'
          name='password'
          value={values.password}
          onChange={handleChange}
          onBlur={handleBlur}
        />
        {touched.password && errors.password && (
          <span style={{ color: 'red' }}>{errors.password}</span>
        )}
      </div>

      {/* Submit error */}
      {errors.submit && <div style={{ color: 'red' }}>{errors.submit}</div>}

      {/* Buttons */}
      <button
        type='submit'
        disabled={isSubmitting}
      >
        {isSubmitting ? 'Submitting...' : 'Register'}
      </button>
      <button
        type='button'
        onClick={reset}
      >
        Reset
      </button>
    </form>
  );
}

🎯 useForm Benefits:

  1. Reusable: Dùng cho bất kỳ form nào
  2. Flexible: Pass custom validate, onSubmit
  3. Complete: Handles values, errors, touched, submitting
  4. Clean components: Form logic extracted

🔨 PHẦN 3: BÀI TẬP THỰC HÀNH (60 phút)

⭐ Level 1: Áp Dụng Concept (15 phút)

jsx
/**
 * 🎯 Mục tiêu: Create useToggle custom hook
 * ⏱️ Thời gian: 15 phút
 * 🚫 KHÔNG dùng: Context, useMemo, useCallback
 *
 * Requirements:
 * 1. Hook quản lý boolean state (true/false)
 * 2. Provide: value, toggle(), setTrue(), setFalse()
 * 3. Optional initial value (default false)
 *
 * Usage:
 * const { value, toggle, setTrue, setFalse } = useToggle(false);
 *
 * 💡 Gợi ý:
 * - Dùng useState (simple case, không cần useReducer)
 * - Return object với 4 properties
 * - Memoize functions? NO - chưa học useCallback!
 */

// TODO: Implement useToggle hook

// Starter:
function useToggle(initialValue = false) {
  // TODO: Implement
}

// Test component:
function ToggleDemo() {
  const modal = useToggle(false);
  const sidebar = useToggle(true);

  return (
    <div>
      <button onClick={modal.toggle}>Toggle Modal</button>
      <button onClick={modal.setTrue}>Open Modal</button>
      <button onClick={modal.setFalse}>Close Modal</button>
      {modal.value && <div>Modal is open!</div>}

      <button onClick={sidebar.toggle}>Toggle Sidebar</button>
      {sidebar.value && <div>Sidebar visible</div>}
    </div>
  );
}
💡 Solution
jsx
/**
 * Custom hook quản lý trạng thái boolean với các hàm điều khiển tiện lợi
 * @param {boolean} [initialValue=false] - Giá trị ban đầu
 * @returns {{
 *   value: boolean,
 *   toggle: () => void,
 *   setTrue: () => void,
 *   setFalse: () => void
 * }}
 */
function useToggle(initialValue = false) {
  const [value, setValue] = React.useState(initialValue);

  const toggle = () => setValue((prev) => !prev);
  const setTrue = () => setValue(true);
  const setFalse = () => setValue(false);

  return { value, toggle, setTrue, setFalse };
}

// ────────────────────────────────────────────────
// Ví dụ sử dụng
// ────────────────────────────────────────────────

function ToggleDemo() {
  const modal = useToggle(false);
  const sidebar = useToggle(true);

  return (
    <div>
      <button onClick={modal.toggle}>Toggle Modal</button>
      <button onClick={modal.setTrue}>Open Modal</button>
      <button onClick={modal.setFalse}>Close Modal</button>
      {modal.value && <div>Modal is open!</div>}

      <hr />

      <button onClick={sidebar.toggle}>Toggle Sidebar</button>
      {sidebar.value && <div>Sidebar visible</div>}
    </div>
  );
}

/* Kết quả ví dụ:
- Ban đầu: Modal đóng, Sidebar mở
- Nhấn "Toggle Modal" → Modal mở → nhấn lần nữa → Modal đóng
- Nhấn "Open Modal" → Modal mở (dù trước đó đang đóng)
- Nhấn "Close Modal" → Modal đóng ngay lập tức
- Sidebar có thể bật/tắt độc lập
*/

⭐⭐ Level 2: Nhận Biết Pattern (25 phút)

jsx
/**
 * 🎯 Mục tiêu: Create useLocalStorage hook
 * ⏱️ Thời gian: 25 phút
 *
 * Requirements:
 * - Sync state với localStorage
 * - Auto-save khi state changes
 * - Auto-load từ localStorage on mount
 * - Handle JSON serialization
 * - Handle localStorage errors (quota, private mode)
 *
 * Usage:
 * const [name, setName] = useLocalStorage('username', 'Guest');
 * // name auto loads from localStorage
 * // setName auto saves to localStorage
 *
 * 🤔 DESIGN DECISIONS:
 *
 * Approach A: useState + useEffect
 * - useState for state
 * - useEffect to sync localStorage
 * Pros: Simple, straightforward
 * Cons: 2 separate hooks, potential race conditions
 *
 * Approach B: useReducer
 * - Reducer handles both state + localStorage
 * - Actions: SET_VALUE, LOAD_FROM_STORAGE
 * Pros: Centralized logic, atomic updates
 * Cons: More boilerplate
 *
 * 💭 WHICH APPROACH AND WHY?
 * (Document your decision in comments)
 */

// TODO: Implement useLocalStorage

// Hints:
// - localStorage.getItem(key)
// - localStorage.setItem(key, value)
// - JSON.parse(), JSON.stringify()
// - Try-catch for errors
// - Check if window.localStorage exists (SSR)

// Test:
function LocalStorageDemo() {
  const [name, setName] = useLocalStorage('username', 'Guest');
  const [count, setCount] = useLocalStorage('count', 0);

  return (
    <div>
      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
      <p>Stored name: {name}</p>

      <button onClick={() => setCount(count + 1)}>Count: {count}</button>

      <p>Refresh page - values persist!</p>
    </div>
  );
}
💡 Solution
jsx
/**
 * Custom hook đồng bộ state với localStorage
 * Tự động load khi mount, tự động save khi state thay đổi
 * @template T
 * @param {string} key - Key trong localStorage
 * @param {T} initialValue - Giá trị mặc định nếu chưa có dữ liệu
 * @returns {[T, (value: T | ((val: T) => T)) => void]}
 */
function useLocalStorage(key, initialValue) {
  // Load từ localStorage khi mount (chỉ chạy 1 lần)
  const [storedValue, setStoredValue] = React.useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      // Nếu có dữ liệu → parse, không có → dùng initialValue
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.warn(`Error reading localStorage key “${key}”:`, error);
      return initialValue;
    }
  });

  // Mỗi khi storedValue thay đổi → lưu vào localStorage
  React.useEffect(() => {
    try {
      window.localStorage.setItem(key, JSON.stringify(storedValue));
    } catch (error) {
      console.warn(`Error writing localStorage key “${key}”:`, error);
    }
  }, [key, storedValue]);

  return [storedValue, setStoredValue];
}

// ────────────────────────────────────────────────
// Ví dụ sử dụng
// ────────────────────────────────────────────────

function LocalStorageDemo() {
  const [name, setName] = useLocalStorage('username', 'Guest');
  const [count, setCount] = useLocalStorage('count', 0);

  return (
    <div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
      <h2>useLocalStorage Demo</h2>

      <div style={{ marginBottom: '20px' }}>
        <label>
          Name:{' '}
          <input
            value={name}
            onChange={(e) => setName(e.target.value)}
            style={{ padding: '8px', fontSize: '16px' }}
          />
        </label>
        <p>
          <strong>Stored name:</strong> {name || 'Guest'}
        </p>
      </div>

      <div>
        <button
          onClick={() => setCount(count + 1)}
          style={{ padding: '10px 20px', fontSize: '16px' }}
        >
          Count: {count}
        </button>
      </div>

      <p style={{ marginTop: '30px', color: '#555' }}>
        🔄 Refresh trang hoặc đóng tab → dữ liệu vẫn còn!
      </p>
    </div>
  );
}

/* Kết quả ví dụ:
- Lần đầu mở trang: name = "Guest", count = 0
- Thay đổi input → giá trị tự động lưu vào localStorage
- Tăng count → cũng tự động lưu
- Refresh trang → mọi giá trị được khôi phục chính xác
- Mở DevTools → Application → Local Storage → thấy 2 key: "username" và "count"
*/

Design Decision: Chọn Approach A (useState + useEffect)
Lý do:

  • Đơn giản, dễ hiểu, ít boilerplate
  • Không cần reducer vì chỉ có 1 action chính (set value)
  • useEffect chỉ chạy khi value thay đổi → performance tốt
  • Xử lý lỗi localStorage (private mode, quota exceeded) bằng try/catch
  • Hoạt động tốt với SSR nếu thêm kiểm tra typeof window !== 'undefined'

⭐⭐⭐ Level 3: Kịch Bản Thực Tế (40 phút)

jsx
/**
 * 🎯 Mục tiêu: Create usePagination hook
 * ⏱️ Thời gian: 40 phút
 *
 * 📋 Product Requirements:
 * User Story: "Là developer, tôi muốn pagination logic reusable"
 *
 * ✅ Acceptance Criteria:
 * - [ ] Track current page
 * - [ ] Calculate total pages from total items
 * - [ ] Provide: currentPage, totalPages, goToPage, nextPage, prevPage
 * - [ ] Prevent going < 1 or > totalPages
 * - [ ] Calculate offset for API (skip = (page - 1) * pageSize)
 * - [ ] Reset to page 1 when totalItems changes
 *
 * Usage:
 * const pagination = usePagination({
 *   totalItems: 100,
 *   itemsPerPage: 10,
 *   initialPage: 1
 * });
 *
 * <button onClick={pagination.prevPage} disabled={pagination.currentPage === 1}>
 *   Previous
 * </button>
 * <span>Page {pagination.currentPage} of {pagination.totalPages}</span>
 * <button onClick={pagination.nextPage} disabled={pagination.currentPage === pagination.totalPages}>
 *   Next
 * </button>
 *
 * 🎨 State Shape:
 * {
 *   currentPage: 1,
 *   totalPages: 10,
 *   itemsPerPage: 10,
 *   totalItems: 100,
 *   offset: 0
 * }
 *
 * 🚨 Edge Cases:
 * - totalItems = 0 → totalPages = 0
 * - currentPage > totalPages after totalItems reduces
 * - Negative page numbers
 * - Non-integer inputs
 *
 * 📝 Implementation Checklist:
 * - [ ] useReducer with actions: SET_PAGE, NEXT_PAGE, PREV_PAGE, SET_TOTAL_ITEMS
 * - [ ] Calculate totalPages = Math.ceil(totalItems / itemsPerPage)
 * - [ ] Calculate offset = (currentPage - 1) * itemsPerPage
 * - [ ] useEffect: reset to page 1 nếu totalItems changes
 * - [ ] Validate: page >= 1 && page <= totalPages
 */

// TODO: Implement usePagination

// Test with real API:
function PaginatedUsers() {
  const [totalItems, setTotalItems] = useState(0);

  const pagination = usePagination({
    totalItems,
    itemsPerPage: 5,
    initialPage: 1,
  });

  const { data, loading, error } = useFetch(
    `https://jsonplaceholder.typicode.com/users?_start=${pagination.offset}&_limit=5`,
  );

  // Update total on initial load
  useEffect(() => {
    if (data && totalItems === 0) {
      setTotalItems(10); // Total users in API
    }
  }, [data]);

  return (
    <div>
      {loading && <div>Loading...</div>}
      {error && <div>Error: {error}</div>}
      {data && (
        <>
          <ul>
            {data.map((user) => (
              <li key={user.id}>{user.name}</li>
            ))}
          </ul>

          <div>
            <button
              onClick={pagination.prevPage}
              disabled={pagination.currentPage === 1}
            >
              Previous
            </button>
            <span>
              Page {pagination.currentPage} of {pagination.totalPages}
            </span>
            <button
              onClick={pagination.nextPage}
              disabled={pagination.currentPage === pagination.totalPages}
            >
              Next
            </button>
          </div>
        </>
      )}
    </div>
  );
}
💡 Solution
jsx
/**
 * Custom hook quản lý logic phân trang (pagination)
 * @param {Object} options
 * @param {number} options.totalItems - Tổng số item (thường từ API)
 * @param {number} [options.itemsPerPage=10] - Số item mỗi trang
 * @param {number} [options.initialPage=1] - Trang bắt đầu
 * @returns {{
 *   currentPage: number,
 *   totalPages: number,
 *   itemsPerPage: number,
 *   offset: number,
 *   goToPage: (page: number) => void,
 *   nextPage: () => void,
 *   prevPage: () => void,
 *   canGoNext: boolean,
 *   canGoPrev: boolean
 * }}
 */
function usePagination({
  totalItems,
  itemsPerPage = 10,
  initialPage = 1,
} = {}) {
  const [currentPage, setCurrentPage] = React.useState(initialPage);

  // Tính toán lại khi totalItems thay đổi → reset về trang 1 nếu cần
  React.useEffect(() => {
    // Nếu totalItems giảm mạnh → currentPage có thể vượt quá totalPages
    const newTotalPages = Math.max(1, Math.ceil(totalItems / itemsPerPage));
    if (currentPage > newTotalPages) {
      setCurrentPage(newTotalPages);
    }
    // Nếu totalItems = 0 → về trang 1
    if (totalItems === 0) {
      setCurrentPage(1);
    }
  }, [totalItems, itemsPerPage, currentPage]);

  const totalPages = Math.max(1, Math.ceil(totalItems / itemsPerPage));
  const offset = (currentPage - 1) * itemsPerPage;

  const goToPage = (page) => {
    const target = Math.max(1, Math.min(page, totalPages));
    setCurrentPage(target);
  };

  const nextPage = () => {
    if (currentPage < totalPages) {
      setCurrentPage((prev) => prev + 1);
    }
  };

  const prevPage = () => {
    if (currentPage > 1) {
      setCurrentPage((prev) => prev - 1);
    }
  };

  return {
    currentPage,
    totalPages,
    itemsPerPage,
    offset,
    goToPage,
    nextPage,
    prevPage,
    canGoNext: currentPage < totalPages,
    canGoPrev: currentPage > 1,
  };
}

// ────────────────────────────────────────────────
// Ví dụ sử dụng với API thực tế (jsonplaceholder)
// ────────────────────────────────────────────────

function PaginatedUsers() {
  const [totalItems, setTotalItems] = React.useState(0);

  const pagination = usePagination({
    totalItems,
    itemsPerPage: 5,
    initialPage: 1,
  });

  const { data, loading, error } = useFetch(
    `https://jsonplaceholder.typicode.com/users?_start=${pagination.offset}&_limit=5`,
  );

  // Cập nhật totalItems một lần khi load danh sách đầu tiên
  React.useEffect(() => {
    if (data && totalItems === 0) {
      // jsonplaceholder có 10 users
      setTotalItems(10);
    }
  }, [data, totalItems]);

  return (
    <div style={{ padding: '20px', maxWidth: '600px', margin: '0 auto' }}>
      <h2>Users (Paginated)</h2>

      {loading && <p>Loading users...</p>}
      {error && <p style={{ color: 'red' }}>Error: {error}</p>}

      {data && (
        <>
          <ul style={{ listStyle: 'none', padding: 0 }}>
            {data.map((user) => (
              <li
                key={user.id}
                style={{
                  padding: '12px',
                  borderBottom: '1px solid #eee',
                }}
              >
                <strong>{user.name}</strong>
                <br />
                <small>{user.email}</small>
              </li>
            ))}
          </ul>

          <div
            style={{
              marginTop: '24px',
              display: 'flex',
              alignItems: 'center',
              gap: '16px',
              justifyContent: 'center',
            }}
          >
            <button
              onClick={pagination.prevPage}
              disabled={!pagination.canGoPrev}
              style={{
                padding: '8px 16px',
                cursor: pagination.canGoPrev ? 'pointer' : 'not-allowed',
              }}
            >
              Previous
            </button>

            <span>
              Page <strong>{pagination.currentPage}</strong> of{' '}
              <strong>{pagination.totalPages}</strong>
            </span>

            <button
              onClick={pagination.nextPage}
              disabled={!pagination.canGoNext}
              style={{
                padding: '8px 16px',
                cursor: pagination.canGoNext ? 'pointer' : 'not-allowed',
              }}
            >
              Next
            </button>
          </div>
        </>
      )}
    </div>
  );
}

/* Kết quả ví dụ:
- Trang 1: hiển thị users 1–5
- Nhấn Next → Trang 2: users 6–10
- Nhấn Previous → quay lại Trang 1
- Không thể Next khi đang ở trang 2 (vì total = 10, 2 trang)
- Không thể Previous khi đang ở trang 1
- Nếu totalItems thay đổi (ví dụ API trả về 7 users), tự động điều chỉnh totalPages và reset page nếu cần
*/

⭐⭐⭐⭐ Level 4: Quyết Định Kiến Trúc (60 phút)

jsx
/**
 * 🎯 Mục tiêu: Create useInfiniteScroll hook
 * ⏱️ Thời gian: 60 phút
 *
 * 🏗️ PHASE 1: Research & Design (20 phút)
 *
 * Context: Infinite scroll là common pattern (Twitter, Instagram)
 * Cần reusable hook cho nhiều lists.
 *
 * Design Questions:
 *
 * 1. State Management:
 *    Option A: useState for each piece
 *    Option B: useReducer for combined state
 *    → DECIDE & DOCUMENT
 *
 * 2. Scroll Detection:
 *    Option A: window scroll listener
 *    Option B: IntersectionObserver (sentinel element)
 *    → DECIDE & DOCUMENT
 *
 * 3. Loading More:
 *    Option A: Hook handles fetching
 *    Option B: Hook only detects scroll, component fetches
 *    → DECIDE & DOCUMENT
 *
 * 📝 Design Doc:
 *
 * ## State Management Decision
 * Choose: useReducer
 * Reason: Multiple related states (items, page, hasMore, loading)
 *
 * ## Scroll Detection Decision
 * Choose: IntersectionObserver
 * Reason: Better performance, no scroll event spam
 *
 * ## Fetch Strategy Decision
 * Choose: Hook returns loadMore callback
 * Reason: Flexibility (works with any API)
 *
 * 💻 PHASE 2: Implementation (30 phút)
 *
 * Hook API:
 * const { items, loadMore, loading, hasMore, observer } = useInfiniteScroll({
 *   fetchFunction: (page) => fetch(`/api/items?page=${page}`),
 *   initialPage: 1
 * });
 *
 * Usage:
 * - Append observer ref to sentinel element: <div ref={observer} />
 * - When sentinel visible → auto call loadMore
 *
 * State:
 * {
 *   items: [],
 *   page: 1,
 *   loading: false,
 *   hasMore: true
 * }
 *
 * Actions:
 * - LOAD_START
 * - LOAD_SUCCESS (append items, increment page)
 * - LOAD_ERROR
 * - NO_MORE_DATA
 *
 * 🧪 PHASE 3: Testing (10 phút)
 *
 * Test cases:
 * - Initial load → show first page
 * - Scroll to bottom → load page 2
 * - Continue scrolling → load pages 3, 4...
 * - No more data → stop loading
 * - Error handling → retry option
 */

// TODO: Implement useInfiniteScroll

// Hints:
// - useRef for IntersectionObserver
// - useEffect setup observer
// - Cleanup: observer.disconnect()
// - Check entries[0].isIntersecting

// Test:
function InfinitePhotoGallery() {
  const fetchPhotos = async (page) => {
    const res = await fetch(
      `https://jsonplaceholder.typicode.com/photos?_page=${page}&_limit=10`,
    );
    const data = await res.json();
    return data;
  };

  const { items, loading, hasMore, observerRef } = useInfiniteScroll({
    fetchFunction: fetchPhotos,
    initialPage: 1,
  });

  return (
    <div>
      <div
        style={{
          display: 'grid',
          gridTemplateColumns: 'repeat(3, 1fr)',
          gap: '10px',
        }}
      >
        {items.map((photo) => (
          <img
            key={photo.id}
            src={photo.thumbnailUrl}
            alt={photo.title}
            style={{ width: '100%' }}
          />
        ))}
      </div>

      {loading && <div>Loading more...</div>}
      {hasMore && (
        <div
          ref={observerRef}
          style={{ height: '20px' }}
        />
      )}
      {!hasMore && <div>No more photos</div>}
    </div>
  );
}
💡 Solution
jsx
/**
 * Custom hook hỗ trợ infinite scroll sử dụng IntersectionObserver
 * Hook chỉ quản lý việc phát hiện scroll và trigger loadMore callback,
 * component chịu trách nhiệm fetch data và append items.
 *
 * @param {Object} options
 * @param {Function} options.loadMore - async function gọi để tải thêm dữ liệu
 *                                      nhận tham số currentPage và trả về array items mới
 * @param {number} [options.initialPage=1] - trang bắt đầu
 * @param {boolean} [options.enabled=true] - có bật infinite scroll hay không
 * @returns {{
 *   items: any[],
 *   page: number,
 *   loading: boolean,
 *   hasMore: boolean,
 *   error: string | null,
 *   loadMore: () => Promise<void>,
 *   observerRef: (node: Element | null) => void,
 *   reset: () => void
 * }}
 */
function useInfiniteScroll({
  loadMore, // async (page) => Promise<any[]>
  initialPage = 1,
  enabled = true,
} = {}) {
  const [items, setItems] = React.useState([]);
  const [page, setPage] = React.useState(initialPage);
  const [loading, setLoading] = React.useState(false);
  const [hasMore, setHasMore] = React.useState(true);
  const [error, setError] = React.useState(null);

  const observer = React.useRef(null);
  const sentinelRef = React.useRef(null);

  // Cleanup observer khi unmount
  React.useEffect(() => {
    return () => {
      if (observer.current) {
        observer.current.disconnect();
      }
    };
  }, []);

  // Tạo IntersectionObserver
  React.useEffect(() => {
    if (!enabled || !hasMore || loading) return;

    observer.current = new IntersectionObserver(
      (entries) => {
        if (entries[0].isIntersecting && hasMore && !loading) {
          loadMoreFn();
        }
      },
      { threshold: 0.1 },
    );

    if (sentinelRef.current) {
      observer.current.observe(sentinelRef.current);
    }

    return () => {
      if (observer.current && sentinelRef.current) {
        observer.current.unobserve(sentinelRef.current);
      }
    };
  }, [enabled, hasMore, loading, page]);

  const loadMoreFn = React.useCallback(async () => {
    if (loading || !hasMore) return;

    setLoading(true);
    setError(null);

    try {
      const newItems = await loadMore(page);

      if (!newItems || newItems.length === 0) {
        setHasMore(false);
        return;
      }

      setItems((prev) => [...prev, ...newItems]);
      setPage((prev) => prev + 1);

      // Nếu API trả về ít hơn mong đợi → coi như hết
      if (newItems.length < 5) {
        // ngưỡng tùy ý
        setHasMore(false);
      }
    } catch (err) {
      setError(err.message || 'Failed to load more items');
      setHasMore(false);
    } finally {
      setLoading(false);
    }
  }, [loading, hasMore, page, loadMore]);

  // Manual trigger load more (cho nút "Load more" fallback)
  const manualLoadMore = React.useCallback(() => {
    if (!loading && hasMore) {
      loadMoreFn();
    }
  }, [loading, hasMore, loadMoreFn]);

  // Reset toàn bộ state
  const reset = React.useCallback(() => {
    setItems([]);
    setPage(initialPage);
    setLoading(false);
    setHasMore(true);
    setError(null);
  }, [initialPage]);

  // Ref callback cho sentinel element
  const observerRef = React.useCallback(
    (node) => {
      if (node !== null) {
        sentinelRef.current = node;
        if (observer.current && enabled) {
          observer.current.observe(node);
        }
      }
    },
    [enabled],
  );

  // Load trang đầu tiên tự động nếu enabled
  React.useEffect(() => {
    if (enabled && page === initialPage && items.length === 0 && !loading) {
      loadMoreFn();
    }
  }, [enabled, initialPage, loadMoreFn, items.length, loading, page]);

  return {
    items,
    page,
    loading,
    hasMore,
    error,
    loadMore: manualLoadMore,
    observerRef,
    reset,
  };
}

// ────────────────────────────────────────────────
// Ví dụ sử dụng
// ────────────────────────────────────────────────

function InfinitePhotoGallery() {
  const fetchPhotos = async (page) => {
    const res = await fetch(
      `https://jsonplaceholder.typicode.com/photos?_page=${page}&_limit=10`,
    );
    if (!res.ok) throw new Error('Network response was not ok');
    return res.json();
  };

  const { items, loading, hasMore, error, observerRef } = useInfiniteScroll({
    loadMore: fetchPhotos,
    initialPage: 1,
    enabled: true,
  });

  return (
    <div style={{ padding: '20px' }}>
      <h2>Infinite Photo Gallery</h2>

      {error && (
        <div style={{ color: 'red', marginBottom: '16px' }}>
          {error}
          <button
            onClick={() => {
              // Có thể thêm retry logic ở đây
              window.location.reload();
            }}
            style={{ marginLeft: '12px' }}
          >
            Retry
          </button>
        </div>
      )}

      <div
        style={{
          display: 'grid',
          gridTemplateColumns: 'repeat(auto-fill, minmax(180px, 1fr))',
          gap: '16px',
        }}
      >
        {items.map((photo) => (
          <div
            key={photo.id}
            style={{
              border: '1px solid #ddd',
              borderRadius: '8px',
              overflow: 'hidden',
            }}
          >
            <img
              src={photo.thumbnailUrl}
              alt={photo.title}
              style={{ width: '100%', height: 'auto', display: 'block' }}
              loading='lazy'
            />
            <div style={{ padding: '8px', fontSize: '14px' }}>
              {photo.title.substring(0, 40)}
              {photo.title.length > 40 ? '...' : ''}
            </div>
          </div>
        ))}
      </div>

      {loading && (
        <div
          style={{ textAlign: 'center', padding: '40px 0', fontSize: '18px' }}
        >
          Loading more photos...
        </div>
      )}

      {hasMore && !loading && (
        <div
          ref={observerRef}
          style={{
            height: '80px',
            margin: '20px 0',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
          }}
        >
          {/* Sentinel element – khi scroll đến đây sẽ tự động load */}
        </div>
      )}

      {!hasMore && items.length > 0 && (
        <div style={{ textAlign: 'center', padding: '40px 0', color: '#666' }}>
          No more photos to load
        </div>
      )}

      {items.length === 0 && !loading && !error && (
        <div style={{ textAlign: 'center', padding: '60px 0' }}>
          Loading initial photos...
        </div>
      )}
    </div>
  );
}

/* Kết quả ví dụ:
- Trang tự động tải 10 ảnh đầu tiên khi mount
- Khi scroll xuống gần cuối (sentinel element hiện ra) → tự động tải trang tiếp theo
- Mỗi lần load thêm 10 ảnh, append vào danh sách
- Khi API hết dữ liệu (hoặc trả về mảng rỗng) → hasMore = false, hiển thị thông báo "No more photos"
- Hỗ trợ error handling và lazy loading ảnh
- Có thể reset bằng cách gọi hook.reset() nếu cần
*/

Design Decisions (tóm tắt):

  • State Management → useState riêng lẻ (không dùng useReducer)
    → Đơn giản hơn, ít boilerplate, đủ cho trường hợp này

  • Scroll Detection → IntersectionObserver (sentinel)
    → Hiệu suất tốt, không gây lag như window scroll listener

  • Fetch Strategy → Hook chỉ trigger loadMore callback
    → Linh hoạt: component quyết định cách fetch (useFetch, axios, fetch, graphql…)

⭐⭐⭐⭐⭐ Level 5: Production Challenge (90 phút)

jsx
/**
 * 🎯 Mục tiêu: Build useDataTable Hook Suite
 * ⏱️ Thời gian: 90 phút
 *
 * 📋 Feature Specification:
 *
 * "Reusable Data Table System" - Hook suite cho data tables
 *
 * Hooks to Build:
 * 1. useDataTable (main hook - composes others)
 * 2. useTableSort
 * 3. useTableFilter
 * 4. useTableSelection
 * 5. useTablePagination (from Level 3)
 *
 * 🏗️ Technical Design:
 *
 * 1. Hook Composition Pattern:
 * function useDataTable(data, config) {
 *   const sort = useTableSort(data, config.defaultSort);
 *   const filter = useTableFilter(sort.data, config.filters);
 *   const selection = useTableSelection(filter.data);
 *   const pagination = useTablePagination({
 *     totalItems: filter.data.length,
 *     itemsPerPage: config.pageSize
 *   });
 *
 *   return {
 *     data: pagination.paginatedData,
 *     sort,
 *     filter,
 *     selection,
 *     pagination
 *   };
 * }
 *
 * 2. useTableSort:
 * - State: { column: null, direction: 'asc' }
 * - Actions: SORT_BY_COLUMN, TOGGLE_DIRECTION
 * - Returns: { data, sortBy, sortDirection, toggleSort }
 *
 * 3. useTableFilter:
 * - State: { filters: {} }
 * - Actions: SET_FILTER, CLEAR_FILTER, CLEAR_ALL
 * - Returns: { data, filters, setFilter, clearFilter }
 *
 * 4. useTableSelection:
 * - State: { selected: Set(), selectAll: false }
 * - Actions: SELECT, DESELECT, TOGGLE, SELECT_ALL, DESELECT_ALL
 * - Returns: { selected, toggleSelection, selectAll, clearSelection }
 *
 * 5. Hook Composition Flow:
 * Raw Data
 *   ↓ useTableSort
 * Sorted Data
 *   ↓ useTableFilter
 * Filtered Data
 *   ↓ useTableSelection (on filtered)
 * Selected Items
 *   ↓ useTablePagination
 * Paginated Data
 *
 * ✅ Production Checklist:
 * - [ ] Each hook testable independently
 * - [ ] Hooks composable (work together)
 * - [ ] Type-safe (document expected shapes)
 * - [ ] Performance (memo filtered/sorted data)
 * - [ ] Edge cases:
 *   - [ ] Empty data
 *   - [ ] Sort by non-existent column
 *   - [ ] Select all with pagination
 *   - [ ] Filter removes selected items
 * - [ ] Demo component using all hooks
 * - [ ] Documentation for each hook
 *
 * 📝 Documentation Template (for each hook):
 *
 * ## useTableSort
 *
 * ### Purpose
 * Handles table column sorting
 *
 * ### API
 * ```js
 * const { data, column, direction, toggleSort } = useTableSort(rawData, defaultSort);
 * ```
 *
 * ### Parameters
 * - rawData: Array - Data to sort
 * - defaultSort: Object - { column: string, direction: 'asc'|'desc' }
 *
 * ### Returns
 * - data: Array - Sorted data
 * - column: string - Current sort column
 * - direction: 'asc'|'desc' - Current direction
 * - toggleSort: Function - (columnName) => void
 *
 * ### Example
 * ```js
 * const users = [{ name: 'Alice', age: 30 }, { name: 'Bob', age: 25 }];
 * const sort = useTableSort(users, { column: 'name', direction: 'asc' });
 * ```
 *
 * 🔍 Self-Review:
 * - [ ] Hooks don't violate Rules of Hooks
 * - [ ] Each hook has single responsibility
 * - [ ] Composition works smoothly
 * - [ ] No prop drilling (hooks provide direct access)
 * - [ ] Documented edge cases handled
 */

// TODO: Implement hook suite

// Starter: Sample data
const sampleUsers = [
  { id: 1, name: 'Alice', age: 30, role: 'Admin', active: true },
  { id: 2, name: 'Bob', age: 25, role: 'User', active: false },
  { id: 3, name: 'Charlie', age: 35, role: 'User', active: true },
  { id: 4, name: 'Diana', age: 28, role: 'Admin', active: true },
  // ... 20 more users
];

// TODO: Implement hooks

// Demo component:
function DataTableDemo() {
  const table = useDataTable(sampleUsers, {
    defaultSort: { column: 'name', direction: 'asc' },
    pageSize: 5,
    filters: ['role', 'active'],
  });

  return (
    <div>
      {/* Filters */}
      <div>
        <select
          onChange={(e) => table.filter.setFilter('role', e.target.value)}
        >
          <option value=''>All Roles</option>
          <option value='Admin'>Admin</option>
          <option value='User'>User</option>
        </select>

        <label>
          <input
            type='checkbox'
            checked={table.filter.filters.active}
            onChange={(e) => table.filter.setFilter('active', e.target.checked)}
          />
          Active Only
        </label>
      </div>

      {/* Selection */}
      <div>
        <button onClick={table.selection.selectAll}>Select All</button>
        <button onClick={table.selection.clearSelection}>
          Clear Selection
        </button>
        <span>Selected: {table.selection.selected.size}</span>
      </div>

      {/* Table */}
      <table>
        <thead>
          <tr>
            <th>
              <input
                type='checkbox'
                checked={table.selection.allSelected}
                onChange={table.selection.toggleSelectAll}
              />
            </th>
            <th onClick={() => table.sort.toggleSort('name')}>
              Name{' '}
              {table.sort.column === 'name' &&
                (table.sort.direction === 'asc' ? '↑' : '↓')}
            </th>
            <th onClick={() => table.sort.toggleSort('age')}>
              Age{' '}
              {table.sort.column === 'age' &&
                (table.sort.direction === 'asc' ? '↑' : '↓')}
            </th>
            <th>Role</th>
            <th>Status</th>
          </tr>
        </thead>
        <tbody>
          {table.data.map((user) => (
            <tr key={user.id}>
              <td>
                <input
                  type='checkbox'
                  checked={table.selection.selected.has(user.id)}
                  onChange={() => table.selection.toggleSelection(user.id)}
                />
              </td>
              <td>{user.name}</td>
              <td>{user.age}</td>
              <td>{user.role}</td>
              <td>{user.active ? 'Active' : 'Inactive'}</td>
            </tr>
          ))}
        </tbody>
      </table>

      {/* Pagination */}
      <div>
        <button
          onClick={table.pagination.prevPage}
          disabled={table.pagination.currentPage === 1}
        >
          Previous
        </button>
        <span>
          Page {table.pagination.currentPage} of {table.pagination.totalPages}
        </span>
        <button
          onClick={table.pagination.nextPage}
          disabled={
            table.pagination.currentPage === table.pagination.totalPages
          }
        >
          Next
        </button>
      </div>
    </div>
  );
}
💡 Solution
jsx
/**
 * === useTableSort ===
 * Hook quản lý sắp xếp bảng theo cột
 */
function useTableSort(data, defaultSort = { column: null, direction: 'asc' }) {
  const [sort, setSort] = React.useState(defaultSort);

  const sortedData = React.useMemo(() => {
    if (!sort.column) return [...data];

    return [...data].sort((a, b) => {
      const aValue = a[sort.column];
      const bValue = b[sort.column];

      if (typeof aValue === 'string' && typeof bValue === 'string') {
        return sort.direction === 'asc'
          ? aValue.localeCompare(bValue)
          : bValue.localeCompare(aValue);
      }

      if (aValue < bValue) return sort.direction === 'asc' ? -1 : 1;
      if (aValue > bValue) return sort.direction === 'asc' ? 1 : -1;
      return 0;
    });
  }, [data, sort.column, sort.direction]);

  const toggleSort = (column) => {
    setSort((prev) => ({
      column,
      direction:
        prev.column === column && prev.direction === 'asc' ? 'desc' : 'asc',
    }));
  };

  return {
    data: sortedData,
    column: sort.column,
    direction: sort.direction,
    toggleSort,
  };
}

/**
 * === useTableFilter ===
 * Hook quản lý lọc dữ liệu theo nhiều trường
 */
function useTableFilter(data, filterableFields = []) {
  const [filters, setFilters] = React.useState({});

  const filteredData = React.useMemo(() => {
    return data.filter((item) => {
      return Object.entries(filters).every(([key, value]) => {
        if (value === undefined || value === '' || value === null) return true;

        const itemValue = item[key];

        if (typeof value === 'boolean') {
          return itemValue === value;
        }

        if (typeof itemValue === 'string') {
          return itemValue.toLowerCase().includes(value.toLowerCase());
        }

        return itemValue === value;
      });
    });
  }, [data, filters]);

  const setFilter = (key, value) => {
    setFilters((prev) => ({
      ...prev,
      [key]: value,
    }));
  };

  const clearFilter = (key) => {
    setFilters((prev) => {
      const next = { ...prev };
      delete next[key];
      return next;
    });
  };

  const clearAllFilters = () => setFilters({});

  return {
    data: filteredData,
    filters,
    setFilter,
    clearFilter,
    clearAllFilters,
  };
}

/**
 * === useTableSelection ===
 * Hook quản lý chọn nhiều bản ghi (checkbox)
 */
function useTableSelection(data) {
  const [selected, setSelected] = React.useState(new Set());

  const toggleSelection = (id) => {
    setSelected((prev) => {
      const next = new Set(prev);
      if (next.has(id)) {
        next.delete(id);
      } else {
        next.add(id);
      }
      return next;
    });
  };

  const selectAll = () => {
    setSelected(new Set(data.map((item) => item.id)));
  };

  const deselectAll = () => {
    setSelected(new Set());
  };

  const toggleSelectAll = () => {
    if (selected.size === data.length) {
      deselectAll();
    } else {
      selectAll();
    }
  };

  const allSelected = data.length > 0 && selected.size === data.length;
  const someSelected = selected.size > 0 && selected.size < data.length;

  return {
    selected,
    toggleSelection,
    selectAll,
    deselectAll,
    toggleSelectAll,
    allSelected,
    someSelected,
    clearSelection: deselectAll,
  };
}

/**
 * === useTablePagination (đơn giản hóa từ Level 3) ===
 */
function useTablePagination({ data, itemsPerPage = 10, initialPage = 1 }) {
  const [currentPage, setCurrentPage] = React.useState(initialPage);

  const totalPages = Math.max(1, Math.ceil(data.length / itemsPerPage));
  const paginatedData = React.useMemo(() => {
    const start = (currentPage - 1) * itemsPerPage;
    return data.slice(start, start + itemsPerPage);
  }, [data, currentPage, itemsPerPage]);

  const goToPage = (page) => {
    const target = Math.max(1, Math.min(page, totalPages));
    setCurrentPage(target);
  };

  return {
    currentPage,
    totalPages,
    paginatedData,
    goToPage,
    nextPage: () => goToPage(currentPage + 1),
    prevPage: () => goToPage(currentPage - 1),
    canGoNext: currentPage < totalPages,
    canGoPrev: currentPage > 1,
  };
}

/**
 * === useDataTable - Composition Hook ===
 * Kết hợp tất cả các tính năng trên thành một API thống nhất
 */
function useDataTable(rawData, config = {}) {
  const { defaultSort, pageSize = 8 } = config;

  const sort = useTableSort(rawData, defaultSort);
  const filter = useTableFilter(sort.data);
  const selection = useTableSelection(filter.data);
  const pagination = useTablePagination({
    data: filter.data,
    itemsPerPage: pageSize,
    initialPage: 1,
  });

  return {
    // Dữ liệu cuối cùng đã được xử lý
    data: pagination.paginatedData,

    // Trạng thái & actions của từng tính năng
    sort,
    filter,
    selection,
    pagination,

    // Tiện ích tổng hợp
    totalItems: rawData.length,
    filteredCount: filter.data.length,
    selectedCount: selection.selected.size,
  };
}

// ────────────────────────────────────────────────
// Demo component sử dụng toàn bộ suite
// ────────────────────────────────────────────────

const sampleUsers = [
  { id: 1, name: 'Alice', age: 30, role: 'Admin', active: true },
  { id: 2, name: 'Bob', age: 25, role: 'User', active: false },
  { id: 3, name: 'Charlie', age: 35, role: 'User', active: true },
  { id: 4, name: 'Diana', age: 28, role: 'Admin', active: true },
  { id: 5, name: 'Eve', age: 42, role: 'Editor', active: true },
  { id: 6, name: 'Frank', age: 31, role: 'User', active: false },
  { id: 7, name: 'Grace', age: 29, role: 'Admin', active: true },
  { id: 8, name: 'Henry', age: 37, role: 'User', active: true },
  // ... có thể thêm nhiều record hơn
];

function DataTableDemo() {
  const table = useDataTable(sampleUsers, {
    defaultSort: { column: 'name', direction: 'asc' },
    pageSize: 5,
  });

  return (
    <div style={{ padding: '20px', fontFamily: 'system-ui, sans-serif' }}>
      <h2>Data Table with Hooks</h2>

      {/* Controls */}
      <div
        style={{
          marginBottom: '24px',
          display: 'flex',
          gap: '16px',
          flexWrap: 'wrap',
        }}
      >
        {/* Filter by role */}
        <select
          onChange={(e) =>
            table.filter.setFilter('role', e.target.value || undefined)
          }
          style={{ padding: '8px' }}
        >
          <option value=''>All Roles</option>
          <option value='Admin'>Admin</option>
          <option value='User'>User</option>
          <option value='Editor'>Editor</option>
        </select>

        {/* Active only */}
        <label style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
          <input
            type='checkbox'
            checked={table.filter.filters.active === true}
            onChange={(e) =>
              table.filter.setFilter(
                'active',
                e.target.checked ? true : undefined,
              )
            }
          />
          Active only
        </label>

        {/* Selection info */}
        <div style={{ marginLeft: 'auto' }}>
          Selected: {table.selectedCount} / {table.filteredCount} items
        </div>
      </div>

      {/* Table */}
      <table style={{ width: '100%', borderCollapse: 'collapse' }}>
        <thead>
          <tr style={{ background: '#f4f4f5' }}>
            <th style={{ padding: '12px', textAlign: 'left', width: '40px' }}>
              <input
                type='checkbox'
                checked={table.selection.allSelected}
                onChange={table.selection.toggleSelectAll}
              />
            </th>
            <th
              onClick={() => table.sort.toggleSort('name')}
              style={{ padding: '12px', cursor: 'pointer', userSelect: 'none' }}
            >
              Name{' '}
              {table.sort.column === 'name' &&
                (table.sort.direction === 'asc' ? '↑' : '↓')}
            </th>
            <th
              onClick={() => table.sort.toggleSort('age')}
              style={{ padding: '12px', cursor: 'pointer', userSelect: 'none' }}
            >
              Age{' '}
              {table.sort.column === 'age' &&
                (table.sort.direction === 'asc' ? '↑' : '↓')}
            </th>
            <th style={{ padding: '12px' }}>Role</th>
            <th style={{ padding: '12px' }}>Status</th>
          </tr>
        </thead>
        <tbody>
          {table.data.map((user) => (
            <tr
              key={user.id}
              style={{ borderBottom: '1px solid #eee' }}
            >
              <td style={{ padding: '12px' }}>
                <input
                  type='checkbox'
                  checked={table.selection.selected.has(user.id)}
                  onChange={() => table.selection.toggleSelection(user.id)}
                />
              </td>
              <td style={{ padding: '12px' }}>{user.name}</td>
              <td style={{ padding: '12px' }}>{user.age}</td>
              <td style={{ padding: '12px' }}>{user.role}</td>
              <td style={{ padding: '12px' }}>
                <span style={{ color: user.active ? '#2e7d32' : '#d32f2f' }}>
                  {user.active ? 'Active' : 'Inactive'}
                </span>
              </td>
            </tr>
          ))}
        </tbody>
      </table>

      {/* Pagination */}
      <div
        style={{
          marginTop: '24px',
          display: 'flex',
          justifyContent: 'center',
          gap: '16px',
          alignItems: 'center',
        }}
      >
        <button
          onClick={table.pagination.prevPage}
          disabled={!table.pagination.canGoPrev}
          style={{ padding: '8px 16px' }}
        >
          Previous
        </button>
        <span>
          Page <strong>{table.pagination.currentPage}</strong> of{' '}
          <strong>{table.pagination.totalPages}</strong>
        </span>
        <button
          onClick={table.pagination.nextPage}
          disabled={!table.pagination.canGoNext}
          style={{ padding: '8px 16px' }}
        >
          Next
        </button>
      </div>
    </div>
  );
}

/* Kết quả ví dụ:
- Sắp xếp theo tên / tuổi khi click header
- Lọc theo role và active/inactive
- Chọn từng dòng / chọn tất cả (chỉ trong trang hiện tại)
- Phân trang tự động 5 record/trang
- Khi lọc → pagination tự cập nhật lại số trang & dữ liệu
- Khi sắp xếp → filter & pagination vẫn hoạt động đúng
*/

// **Ghi chú thiết kế chính:**

// - Mỗi hook có trách nhiệm **đơn lẻ** (Single Responsibility)
// - Composition theo thứ tự logic: sort → filter → selection → pagination
// - Sử dụng **useMemo** để tránh tính toán không cần thiết
// - State được giữ riêng biệt → dễ test từng hook độc lập
// - API tổng hợp ở `useDataTable` giúp component sử dụng rất gọn gàng

// Hy vọng implementation này đủ thực tế để dùng trong dự án production nhỏ đến trung bình!

📊 PHẦN 4: SO SÁNH PATTERNS (30 phút)

Bảng So Sánh: Custom Hook Strategies

StrategyComplexityReusabilityTestabilityUse When
Inline Logic✅ Simple❌ None❌ HardOne-off, component-specific
Extract Function✅ Simple⚠️ Medium✅ EasyPure logic, no hooks
Custom Hook⚠️ Medium✅ High✅ EasyReusable stateful logic
Hook Composition❌ Complex✅✅ Very High✅ ModularComplex features

Decision Tree: Khi nào tạo Custom Hook?

START: Cần refactor logic?

├─ Logic chỉ dùng 1 component?
│  └─ YES → Keep inline ✅
│     No need custom hook yet

├─ Logic pure (no hooks)?
│  └─ YES → Extract regular function ✅
│     Example: validation, formatting

├─ Logic dùng hooks (useState, useEffect)?
│  └─ YES
│     │
│     ├─ Dùng ở 2+ components?
│     │  └─ YES → Custom Hook ✅
│     │     Example: useFetch, useForm
│     │
│     ├─ Logic phức tạp (>50 lines)?
│     │  └─ YES → Custom Hook ✅
│     │     Even if 1 component (readability)
│     │
│     └─ Will likely reuse in future?
│        └─ YES → Custom Hook ✅
│           Proactive abstraction

└─ Complex feature (multiple concerns)?
   └─ YES → Hook Composition ✅
      Example: useDataTable = sort + filter + pagination

Custom Hook Best Practices

✅ DO:

jsx
// ✅ Descriptive naming
function useFetchUser(userId) { ... }
function useDebounce(value, delay) { ... }
function useLocalStorage(key, initialValue) { ... }

// ✅ Return consistent structure
function useFetch(url) {
  return { data, loading, error }; // Always same shape
}

// ✅ Accept config object for flexibility
function useTable(data, config = {}) {
  const {
    defaultSort = { column: null, direction: 'asc' },
    pageSize = 10,
  } = config;
}

// ✅ Document with JSDoc
/**
 * Fetches data from URL
 * @param {string} url - API endpoint
 * @returns {{ data, loading, error }}
 */
function useFetch(url) { ... }

❌ DON'T:

jsx
// ❌ Generic naming
function useData() { ... } // Data from where?

// ❌ Inconsistent returns
function useFetch(url) {
  if (loading) return [null, true, null];
  return { data, loading, error }; // Different types!
}

// ❌ Too many parameters
function useFetch(url, method, headers, body, timeout, retries) { ... }
// Better: useFetch(url, options)

// ❌ Side effects in hook body
function useFetch(url) {
  console.log('Fetching...'); // Don't log in hook!
  // Put logging in useEffect
}

Hook Composition Patterns

Pattern 1: Sequential Composition

jsx
// Each hook depends on previous
function useDataTable(data) {
  const sorted = useSort(data);
  const filtered = useFilter(sorted);
  const paginated = usePagination(filtered);
  return paginated;
}

Pattern 2: Parallel Composition

jsx
// Hooks independent
function useForm(initialValues) {
  const validation = useValidation(initialValues);
  const storage = useLocalStorage('form', initialValues);
  const submit = useSubmit();

  // Combine results
  return { ...validation, ...storage, ...submit };
}

Pattern 3: Conditional Composition

jsx
// Use hooks conditionally (WRONG!)
function useConditional(shouldFetch) {
  // ❌ VIOLATES RULES OF HOOKS
  if (shouldFetch) {
    const data = useFetch('/api/data');
  }
}

// ✅ CORRECT
function useConditional(shouldFetch) {
  const { data } = useFetch(shouldFetch ? '/api/data' : null);
  // Hook always called, but fetch is conditional
}

🧪 PHẦN 5: DEBUG LAB (20 phút)

Bug 1: Shared State Misconception 🐛

jsx
// ❌ CODE/CONCEPT SAI
function useCounter() {
  const [count, setCount] = useState(0);

  const increment = () => setCount(count + 1);

  return { count, increment };
}

function ComponentA() {
  const { count, increment } = useCounter();
  return <button onClick={increment}>A: {count}</button>;
}

function ComponentB() {
  const { count, increment } = useCounter();
  return <button onClick={increment}>B: {count}</button>;
}

function App() {
  return (
    <div>
      <ComponentA />
      <ComponentB />
    </div>
  );
}

// 🤔 User clicks ComponentA button
// Expected: ComponentA = 1, ComponentB = 1 (shared state?)
// Actual: ComponentA = 1, ComponentB = 0
// WHY?

❓ Câu hỏi:

  1. Tại sao count không shared giữa A và B?
  2. Custom hook share gì?
  3. Làm sao share state thật sự?

💡 Giải thích:

  • Custom hooks share LOGIC, not STATE
  • Mỗi component gọi hook → separate instance
  • A có state riêng, B có state riêng
  • Giống như 2 components cùng dùng useState - mỗi cái có state riêng!

✅ Fix (nếu cần shared state):

jsx
// Option 1: Lift state up
function App() {
  const { count, increment } = useCounter();

  return (
    <div>
      <ComponentA
        count={count}
        increment={increment}
      />
      <ComponentB
        count={count}
        increment={increment}
      />
    </div>
  );
}

// Option 2: Context (will learn later)
// const CountContext = createContext();

Bug 2: Rules of Hooks Violation 🐛

jsx
// ❌ CODE BỊ LỖI
function useFetchOnCondition(shouldFetch, url) {
  // 🐛 Conditional hook call!
  if (shouldFetch) {
    const { data, loading } = useFetch(url);
    return { data, loading };
  }

  return { data: null, loading: false };
}

// React Error: "Rendered more hooks than during the previous render"

❓ Câu hỏi:

  1. Vấn đề gì với code?
  2. Tại sao React throw error?
  3. Làm sao fix?

💡 Giải thích:

  • Rules of Hooks: Hooks phải gọi trong SAME ORDER mỗi render
  • Conditional → order thay đổi → React confused
  • Render 1: shouldFetch=true → 1 hook called
  • Render 2: shouldFetch=false → 0 hooks called
  • React: "WTF? Where did hook go?"

✅ Fix:

jsx
// ✅ Always call hook, conditionally use result
function useFetchOnCondition(shouldFetch, url) {
  const { data, loading } = useFetch(shouldFetch ? url : null);
  return { data, loading };
}

// useFetch internally handles null URL:
function useFetch(url) {
  useEffect(() => {
    if (!url) return; // Skip fetch if no URL
    // ... fetch logic
  }, [url]);
}

Bug 3: Stale Closure in Custom Hook 🐛

jsx
// ❌ CODE BỊ LỖI
function useInterval(callback, delay) {
  useEffect(() => {
    const interval = setInterval(callback, delay);
    return () => clearInterval(interval);
  }, [delay]); // 🐛 Missing callback in deps!
}

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

  useInterval(() => {
    console.log('Count:', count); // 🐛 Always logs 0!
    setCount(count + 1); // 🐛 Always sets 1!
  }, 1000);

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

// Bug: count increments to 1, then stops!
// Log always shows "Count: 0"

❓ Câu hỏi:

  1. Tại sao count luôn 0 trong callback?
  2. Tại sao count chỉ tăng lên 1 rồi dừng?
  3. Làm sao fix?

💡 Giải thích:

  • Stale closure: callback captures count từ initial render
  • useEffect chỉ re-run khi delay changes
  • callback không update → luôn thấy count = 0
  • setCount(0 + 1) → count = 1, rồi stuck

✅ Fix:

jsx
// ✅ Option 1: Include callback in deps
function useInterval(callback, delay) {
  useEffect(() => {
    const interval = setInterval(callback, delay);
    return () => clearInterval(interval);
  }, [callback, delay]); // Add callback
}
// Problem: callback changes every render → interval resets!

// ✅ Option 2: useRef to store latest callback
function useInterval(callback, delay) {
  const savedCallback = useRef();

  // Update ref khi callback changes
  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  // Setup interval
  useEffect(() => {
    const tick = () => {
      savedCallback.current(); // Call latest callback
    };

    const interval = setInterval(tick, delay);
    return () => clearInterval(interval);
  }, [delay]); // Only re-run khi delay changes
}

// ✅ Option 3: Functional update
function Counter() {
  const [count, setCount] = useState(0);

  useInterval(() => {
    setCount((prev) => prev + 1); // ✅ No dependency on count!
  }, 1000);
}

✅ PHẦN 6: TỰ ĐÁNH GIÁ (15 phút)

Knowledge Check

Đánh dấu ✅ khi bạn tự tin:

Custom Hook Basics:

  • [ ] Tôi hiểu custom hook là gì
  • [ ] Tôi biết naming convention (must start with 'use')
  • [ ] Tôi hiểu custom hooks share logic, not state
  • [ ] Tôi biết khi nào nên extract custom hook

Hook Creation:

  • [ ] Tôi biết cách extract logic thành hook
  • [ ] Tôi biết cách accept parameters
  • [ ] Tôi biết cách return values (object vs array)
  • [ ] Tôi biết cách document hooks (JSDoc)

Hook Composition:

  • [ ] Tôi biết cách compose multiple hooks
  • [ ] Tôi hiểu sequential vs parallel composition
  • [ ] Tôi biết cách handle dependencies giữa hooks
  • [ ] Tôi tránh được conditional hook calls

Rules of Hooks:

  • [ ] Tôi hiểu Rules of Hooks
  • [ ] Tôi không call hooks conditionally
  • [ ] Tôi không call hooks in loops
  • [ ] Tôi không call hooks in regular functions

Code Review Checklist

Hook Design:

  • [ ] Naming: starts with 'use', descriptive
  • [ ] Single responsibility
  • [ ] Configurable via parameters
  • [ ] Consistent return type

Implementation:

  • [ ] No Rules of Hooks violations
  • [ ] Dependencies correct (no missing, no extra)
  • [ ] Cleanup functions where needed
  • [ ] Error handling

Reusability:

  • [ ] No hardcoded values
  • [ ] Flexible via config
  • [ ] Works in different contexts
  • [ ] Well documented

Testing:

  • [ ] Can be tested independently
  • [ ] Clear inputs/outputs
  • [ ] Edge cases handled
  • [ ] No side effects in hook body

🏠 BÀI TẬP VỀ NHÀ

Bắt buộc (30 phút)

Bài 1: useDebounce Hook

Requirements:

  1. Accept value và delay
  2. Return debounced value
  3. Delay updates by specified ms
  4. Cancel pending update on value change
  5. Cancel pending update on unmount

Usage:

jsx
function SearchBox() {
  const [searchTerm, setSearchTerm] = useState('');
  const debouncedSearch = useDebounce(searchTerm, 500);

  useEffect(() => {
    if (debouncedSearch) {
      // API call with debouncedSearch
    }
  }, [debouncedSearch]);

  return (
    <input
      value={searchTerm}
      onChange={(e) => setSearchTerm(e.target.value)}
    />
  );
}

Hints:

  • Use useState for debounced value
  • Use useEffect with setTimeout
  • Return cleanup function to clear timeout
💡 Solution
jsx
/**
 * Custom hook debounce giá trị đầu vào
 * Trì hoãn việc cập nhật giá trị cho đến khi người dùng ngừng thay đổi trong khoảng delay
 * @param {any} value - Giá trị cần debounce (thường là string từ input)
 * @param {number} [delay=500] - Thời gian chờ (ms) trước khi cập nhật giá trị
 * @returns {any} Giá trị đã được debounce
 */
function useDebounce(value, delay = 500) {
  const [debouncedValue, setDebouncedValue] = React.useState(value);

  React.useEffect(() => {
    // Thiết lập timer để cập nhật giá trị sau delay
    const timer = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    // Cleanup: hủy timer nếu value thay đổi trước khi delay kết thúc
    // hoặc khi component unmount
    return () => {
      clearTimeout(timer);
    };
  }, [value, delay]); // Chỉ chạy lại khi value hoặc delay thay đổi

  return debouncedValue;
}

// ────────────────────────────────────────────────
// Ví dụ sử dụng
// ────────────────────────────────────────────────

function SearchBox() {
  const [searchTerm, setSearchTerm] = React.useState('');
  const debouncedSearch = useDebounce(searchTerm, 500);

  // Mỗi khi debouncedSearch thay đổi → thực hiện tìm kiếm / gọi API
  React.useEffect(() => {
    if (debouncedSearch.trim()) {
      console.log('Tìm kiếm với từ khóa:', debouncedSearch);
      // Ví dụ: fetch(`/api/search?q=${debouncedSearch}`)
    }
  }, [debouncedSearch]);

  return (
    <div style={{ padding: '20px', maxWidth: '500px', margin: '0 auto' }}>
      <h2>Search with Debounce</h2>

      <input
        type='text'
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
        placeholder='Nhập từ khóa để tìm kiếm...'
        style={{
          width: '100%',
          padding: '12px',
          fontSize: '16px',
          borderRadius: '6px',
          border: '1px solid #ccc',
        }}
      />

      <div style={{ marginTop: '20px' }}>
        <p>
          <strong>Giá trị đang nhập:</strong> {searchTerm || '(chưa nhập)'}
        </p>
        <p style={{ color: debouncedSearch ? '#2e7d32' : '#757575' }}>
          <strong>Giá trị debounce (sau {500}ms):</strong>{' '}
          {debouncedSearch || '(đang chờ...)'}
        </p>
      </div>

      <small style={{ color: '#666', display: 'block', marginTop: '16px' }}>
        API / tìm kiếm chỉ được gọi khi bạn ngừng gõ ít nhất 500ms
      </small>
    </div>
  );
}

/* Kết quả ví dụ:
- Gõ nhanh "react hooks" → debouncedSearch vẫn là "" hoặc giá trị cũ
- Ngừng gõ 500ms → console.log("Tìm kiếm với từ khóa: react hooks")
- Thay đổi input liên tục → không gọi API liên tục, chỉ gọi 1 lần sau khi ngừng gõ
- Xóa hết input → sau 500ms debouncedSearch trở thành ""
*/

Nâng cao (60 phút)

Bài 2: useUndo Hook

Requirements:

  1. Track state history
  2. Provide: state, setState, undo, redo, canUndo, canRedo
  3. Limit history size (e.g., max 10 states)
  4. Clear future history when new state set
  5. Works with any state type

State shape:

jsx
{
  past: [state1, state2, ...],
  present: currentState,
  future: [state3, state4, ...]
}

Actions:

  • SET (new state)
  • UNDO
  • REDO
  • CLEAR_HISTORY

Usage:

jsx
function DrawingApp() {
  const {
    state: canvas,
    setState: setCanvas,
    undo,
    redo,
    canUndo,
    canRedo,
    reset,
  } = useUndo([]);

  return (
    <div>
      <button
        onClick={undo}
        disabled={!canUndo}
      >
        Undo
      </button>
      <button
        onClick={redo}
        disabled={!canRedo}
      >
        Redo
      </button>
      <button onClick={reset}>Clear</button>
      <Canvas
        data={canvas}
        onChange={setCanvas}
      />
    </div>
  );
}
💡 Solution
jsx
/**
 * Custom hook hỗ trợ undo / redo cho bất kỳ giá trị state nào
 * Giữ lịch sử các thay đổi (past + future) với giới hạn kích thước
 * @template T
 * @param {T} initialValue - Giá trị ban đầu
 * @param {number} [maxHistory=10] - Số lượng trạng thái tối đa trong history (past + future)
 * @returns {{
 *   state: T,
 *   setState: (newState: T | ((prev: T) => T)) => void,
 *   undo: () => void,
 *   redo: () => void,
 *   canUndo: boolean,
 *   canRedo: boolean,
 *   reset: () => void,
 *   clearHistory: () => void
 * }}
 */
function useUndo(initialValue, maxHistory = 10) {
  const initialState = {
    past: [],
    present: initialValue,
    future: [],
  };

  const [history, setHistory] = React.useState(initialState);

  const { past, present, future } = history;

  const canUndo = past.length > 0;
  const canRedo = future.length > 0;

  const setState = React.useCallback(
    (newState) => {
      setHistory((prev) => {
        const nextPresent =
          typeof newState === 'function' ? newState(prev.present) : newState;

        // Nếu giá trị không thay đổi → không thêm vào history
        if (nextPresent === prev.present) {
          return prev;
        }

        // Thêm present hiện tại vào past
        // Giới hạn kích thước past
        const newPast = [...prev.past, prev.present];
        if (newPast.length > maxHistory) {
          newPast.shift(); // xóa trạng thái cũ nhất
        }

        return {
          past: newPast,
          present: nextPresent,
          future: [], // xóa future khi có thay đổi mới
        };
      });
    },
    [maxHistory],
  );

  const undo = React.useCallback(() => {
    if (!canUndo) return;

    setHistory((prev) => {
      const previous = prev.past[prev.past.length - 1];
      const newPast = prev.past.slice(0, -1);

      return {
        past: newPast,
        present: previous,
        future: [prev.present, ...prev.future],
      };
    });
  }, [canUndo]);

  const redo = React.useCallback(() => {
    if (!canRedo) return;

    setHistory((prev) => {
      const next = prev.future[0];
      const newFuture = prev.future.slice(1);

      return {
        past: [...prev.past, prev.present],
        present: next,
        future: newFuture,
      };
    });
  }, [canRedo]);

  const reset = React.useCallback(() => {
    setHistory({
      past: [],
      present: initialValue,
      future: [],
    });
  }, [initialValue]);

  const clearHistory = React.useCallback(() => {
    setHistory((prev) => ({
      past: [],
      present: prev.present,
      future: [],
    }));
  }, []);

  return {
    state: present,
    setState,
    undo,
    redo,
    canUndo,
    canRedo,
    reset,
    clearHistory,
  };
}

// ────────────────────────────────────────────────
// Ví dụ sử dụng: Ứng dụng vẽ đơn giản (mảng điểm)
// ────────────────────────────────────────────────

function DrawingApp() {
  const {
    state: points,
    setState: setPoints,
    undo,
    redo,
    canUndo,
    canRedo,
    reset,
  } = useUndo([], 15); // Giới hạn 15 bước history

  const handleClick = (e) => {
    const rect = e.currentTarget.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;

    setPoints((prev) => [...prev, { x, y }]);
  };

  return (
    <div style={{ padding: '20px', fontFamily: 'system-ui, sans-serif' }}>
      <h2>useUndo Demo - Simple Drawing</h2>

      <div
        style={{
          marginBottom: '16px',
          display: 'flex',
          gap: '12px',
          flexWrap: 'wrap',
        }}
      >
        <button
          onClick={undo}
          disabled={!canUndo}
        >
          Undo ({canUndo ? '✓' : '✗'})
        </button>
        <button
          onClick={redo}
          disabled={!canRedo}
        >
          Redo ({canRedo ? '✓' : '✗'})
        </button>
        <button onClick={reset}>Reset / Clear All</button>
      </div>

      <div
        onClick={handleClick}
        style={{
          width: '500px',
          height: '400px',
          border: '2px solid #333',
          background: '#f8f9fa',
          position: 'relative',
          cursor: 'crosshair',
          overflow: 'hidden',
        }}
      >
        {points.map((point, index) => (
          <div
            key={index}
            style={{
              position: 'absolute',
              left: point.x - 6,
              top: point.y - 6,
              width: '12px',
              height: '12px',
              background: '#1976d2',
              borderRadius: '50%',
              transform: 'translate(-50%, -50%)',
            }}
          />
        ))}
      </div>

      <div style={{ marginTop: '16px', color: '#555' }}>
        Points: {points.length} | History: {canUndo ? past.length : 0} past,{' '}
        {canRedo ? future.length : 0} future
      </div>

      <small style={{ color: '#777', display: 'block', marginTop: '8px' }}>
        Click vào khung để vẽ điểm • Undo/Redo để quay lại hoặc tiến tới
      </small>
    </div>
  );
}

/* Kết quả ví dụ:
- Click nhiều lần vào khung → tạo các điểm màu xanh
- Nhấn Undo → xóa điểm cuối cùng (có thể Undo nhiều bước)
- Nhấn Redo → khôi phục điểm vừa bị Undo
- Reset → xóa hết điểm và history
- Sau 15 bước → các bước cũ nhất tự động bị xóa khỏi history (giới hạn maxHistory)
- Không thể Undo khi không còn lịch sử (nút disable)
*/

📚 TÀI LIỆU THAM KHẢO

Bắt buộc đọc

  1. React Docs - Reusing Logic with Custom Hooks:

  2. Rules of Hooks:

Đọc thêm

  1. useHooks Collection:

  2. React Hook Patterns:


🔗 KẾT NỐI KIẾN THỨC

Kiến thức nền (Đã học)

  • Ngày 11-14: useState

    • Foundation cho custom hooks
    • useFetch, useToggle dùng useState
  • Ngày 16-20: useEffect

    • Critical cho side effects in hooks
    • Cleanup patterns
  • Ngày 21-22: useRef

    • Store mutable values
    • useInterval pattern (savedCallback ref)
  • Ngày 26-28: useReducer

    • Complex state in hooks
    • useForm, usePagination patterns

Hướng tới (Sẽ học)

  • Ngày 30: Project 4 - Shopping Cart

    • Apply custom hooks learned today
    • Hook composition in practice
  • Ngày 31-34: Performance Hooks

    • useMemo, useCallback
    • Optimize custom hooks
    • Memoize expensive operations
  • Phase 5: Context API

    • Custom hooks + Context
    • Global state management
    • useAuth, useTheme patterns

💡 SENIOR INSIGHTS

Cân Nhắc Production

1. When NOT to create custom hook:

jsx
// ❌ Over-abstraction
function useButtonClick(onClick) {
  return { onClick }; // Unnecessary!
}

// ❌ One-off logic
function useSpecificBusinessLogic() {
  // Logic chỉ dùng 1 component
  // Keep inline!
}

// ✅ When to extract:
// - Used in 2+ components
// - Complex logic (>50 lines)
// - Reusable across projects

2. Hook Library Organization:

src/
  hooks/
    useAuth.js          # Domain-specific
    useUser.js
    useProduct.js

    useAsync.js         # Generic utilities
    useFetch.js
    useLocalStorage.js

    useTable/           # Complex hooks
      useTableSort.js
      useTableFilter.js
      index.js          # Main composition

3. Versioning Custom Hooks:

jsx
// v1: Simple
function useFetch(url) { ... }

// v2: Add options (backward compatible)
function useFetch(url, options = {}) { ... }

// v3: Breaking change (rename parameter)
function useFetchV3(config) { ... }
// Or: deprecate v2, migrate gradually

4. Testing Strategy:

jsx
// Test custom hook với @testing-library/react-hooks
import { renderHook, act } from '@testing-library/react-hooks';

test('useFetch loads data', async () => {
  const { result, waitForNextUpdate } = renderHook(() => useFetch('/api/data'));

  expect(result.current.loading).toBe(true);

  await waitForNextUpdate();

  expect(result.current.data).toBeDefined();
  expect(result.current.loading).toBe(false);
});

Câu Hỏi Phỏng Vấn

Junior Level:

  1. Q: "Custom hook khác function thông thường như thế nào?"

    Expected:

    • Naming: must start with 'use'
    • Can use other hooks inside
    • Follow Rules of Hooks
    • Regular function không thể dùng hooks
  2. Q: "Viết custom hook quản lý input field"

    Expected:

    jsx
    function useInput(initialValue) {
      const [value, setValue] = useState(initialValue);
    
      const onChange = (e) => setValue(e.target.value);
      const reset = () => setValue(initialValue);
    
      return { value, onChange, reset };
    }

Mid Level:

  1. Q: "2 components dùng cùng custom hook có share state không? Giải thích."

    Expected:

    • NO, mỗi component có state riêng
    • Hook chỉ share logic
    • Demo với code example
    • Explain khi nào cần share state (Context)
  2. Q: "Implement useDebounce hook. Explain use case."

    Expected:

    • Code implementation
    • Use case: search input, auto-save
    • Performance benefits
    • Dependencies chính xác

Senior Level:

  1. Q: "Design custom hook library cho company. Architecture? Best practices? Testing strategy?"

    Expected:

    • Organization (generic vs domain-specific)
    • Documentation standards
    • Versioning strategy
    • TypeScript support
    • Testing (unit + integration)
    • Performance considerations
    • Code review checklist
    • Migration path cho breaking changes

War Stories

Story 1: The useInterval Bug

"App có timer countdown. User reported 'timer stuck at 1'. Sau 3 giờ debug, phát hiện stale closure trong useInterval hook. Callback capture count=0 lúc mount. Fix bằng useRef pattern. Lesson: Understand closure, deps array carefully!" - Senior Engineer

Story 2: Over-Abstraction Hell

"Junior dev tạo hook cho MỌI THỨ. useButtonState, useInputValue, useModalOpen... 50+ hooks, mỗi cái 5 lines. Code review: 'This is over-engineering'. Rollback, giữ lại 10 hooks thật sự reusable. Lesson: Abstraction có cost. Extract khi có clear benefit." - Tech Lead

Story 3: Custom Hook Saved 10k Lines

"E-commerce app có 20 components fetch data. Mỗi cái 100 lines useEffect + reducer. Total 2000 lines duplicate. Extracted useFetch hook → 200 lines. Add retry logic? 1 line change instead of 20. ROI huge. Lesson: Good abstraction pays off!" - CTO


🎯 PREVIEW NGÀY MAI

Ngày 30: ⚡ Project 4 - Shopping Cart

Bạn sẽ build:

  • ✨ Complete shopping cart với useReducer
  • ✨ Custom hooks: useCart, useProducts, useCheckout
  • ✨ Optimistic updates (add/remove items)
  • ✨ localStorage persistence
  • ✨ Discount codes, tax calculation

Chuẩn bị:

  • Hoàn thành bài tập hôm nay
  • Review useReducer patterns (Ngày 26-28)
  • Review custom hooks learned today
  • Nghĩ về shopping cart features cần thiết

🎉 Chúc mừng! Bạn đã hoàn thành Ngày 29!

Bạn giờ đã master:

  • ✅ Custom hooks creation
  • ✅ useFetch, useAsync, useForm patterns
  • ✅ Hook composition
  • ✅ Rules of Hooks
  • ✅ Reusable logic extraction

Tomorrow: Tổng hợp tất cả vào 1 project thực tế! 💪

Personal tech knowledge base