C
React/Hooks/Lesson 12

Custom Hooks

30 min·theory
This chapter
4/5
JavaScript

Custom Hooks

💡 Why should you learn this?

🎯 You can reuse the same logic across multiple components without writing it repeatedly.
💼 It simplifies complex components and makes the code easier to read.
It is an advanced technique that significantly speeds up development in team projects.
🏢 실무에서는
In professional settings, repetitive logic such as 'fetching user data,' 'form validation,' and 'managing local storage' is extracted into custom Hooks and reused across multiple pages. In large-scale team projects, a shared Hook library is built so everyone can use it together.

Concept

Custom hooks are a core pattern in React 19 that separates state logic from components and turns it into reusable, testable units. In professional development, they are an essential technique for reducing component complexity and boosting team-wide productivity.

Why does it matter?

In large-scale services, when multiple components repeatedly implement the same state logic (API calls, form validation, localStorage, etc.), code duplication and bugs increase rapidly. For example, if login state management logic is scattered across 10 components, changing the authentication method requires modifying all 10 places.

Core concept

Custom hooks are like a shared toolbox. Just as you would store commonly used tools like screwdrivers and wrenches in one toolbox rather than keeping a separate set in every room, you centralize the state logic shared across multiple components by extracting it into a custom hook.

Key points

  • Created by combining React built-in hooks using a function name that starts with use
  • Returns state and state manipulation functions as an object or array to maximize reusability
  • Completely decoupled from component logic, making unit testing very straightforward
💻 Anti-pattern — All logic inside the component
function UserProfile() {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const fetchUser = async () => {
      setLoading(true);
      setError(null);
      try {
        const response = await fetch('/api/user');
        const userData = await response.json();
        setUser(userData);
      } catch (err) {
        setError('Could not load user information');
      } finally {
        setLoading(false);
      }
    };
    fetchUser();
  }, []);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  return <div>{user?.name}</div>;
}
💻 2025 recommended pattern — Testable custom hooks
// hooks/useApi.ts
function useApi<T>(url: string) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const fetchData = useCallback(async () => {
    setLoading(true);
    setError(null);
    try {
      const response = await fetch(url);
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      const result = await response.json();
      setData(result);
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Unknown error');
    } finally {
      setLoading(false);
    }
  }, [url]);

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

  return { data, loading, error, refetch: fetchData } as const;
}

// Usage in component
function UserProfile() {
  const { data: user, loading, error } = useApi<User>('/api/user');

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

// __tests__/useApi.test.ts
import { renderHook, waitFor } from '@testing-library/react';
import { useApi } from '../hooks/useApi';

test('should return data on successful API call', async () => {
  global.fetch = jest.fn().mockResolvedValue({
    ok: true,
    json: () => Promise.resolve({ id: 1, name: 'Kim Gaebal' })
  });

  const { result } = renderHook(() => useApi('/api/user'));

  await waitFor(() => {
    expect(result.current.data).toEqual({ id: 1, name: 'Kim Gaebal' });
  });
});
💻 Real-world pattern — Form management custom hook
// hooks/useForm.ts
type ValidationRule<T> = {
  [K in keyof T]?: (value: T[K]) => string | null;
};

function useForm<T extends Record<string, any>>(
  initialValues: T,
  validationRules?: ValidationRule<T>
) {
  const [values, setValues] = useState<T>(initialValues);
  const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({});
  const [touched, setTouched] = useState<Partial<Record<keyof T, boolean>>>({});

  const setValue = useCallback((name: keyof T, value: any) => {
    setValues(prev => ({ ...prev, [name]: value }));
    
    // Real-time validation
    if (validationRules?.[name]) {
      const error = validationRules[name](value);
      setErrors(prev => ({ ...prev, [name]: error }));
    }
  }, [validationRules]);

  const setTouched = useCallback((name: keyof T) => {
    setTouched(prev => ({ ...prev, [name]: true }));
  }, []);

  const isValid = useMemo(() => {
    return Object.values(errors).every(error => !error);
  }, [errors]);

  const reset = useCallback(() => {
    setValues(initialValues);
    setErrors({});
    setTouched({});
  }, [initialValues]);

  return {
    values,
    errors,
    touched,
    setValue,
    setTouched,
    isValid,
    reset
  } as const;
}

// Usage example
function LoginForm() {
  const { values, errors, setValue, isValid } = useForm(
    { email: '', password: '' },
    {
      email: (value) => !value.includes('@') ? 'Invalid email format' : null,
      password: (value) => value.length < 8 ? 'Password must be at least 8 characters' : null
    }
  );

  return (
    <form>
      <input 
        value={values.email}
        onChange={(e) => setValue('email', e.target.value)}
      />
      {errors.email && <span>{errors.email}</span>}
      
      <button type="submit" disabled={!isValid}>
        Login
      </button>
    </form>
  );
}

💡 ⚠️ Common mistakes

  • Calling other hooks conditionally inside a hook, violating React hook rules (hooks must always be called in the same order)
  • Incorrectly setting the dependency array, causing infinite re-renders (mistakes managing useCallback and useEffect dependencies)
  • Packing too many responsibilities into a single custom hook, violating the single responsibility principle (a large hook like useEverything)

💡 🎯 Interview prep

Q: What is the difference between a custom hook and a regular function?
Q: How do you test a custom hook?
Q: When do you think you should create a custom hook?

Hints: 1) Custom hooks can use React built-in hooks and participate in the component lifecycle. 2) Unit testing is possible using renderHook and act. 3) Consider extracting when the same state logic is repeated in two or more components.

⚛️ React Pattern — Custom Hooks

Learn how custom hooks are used in React step by step, with code examples.
1 📦 1. Declare State
Creating internal component state with `useState`
const [count, setCount] = useState(0);
2 👁️ 2. Rendering
Display the state value in JSX
return <h1>Current count: {count}</h1>;
3 🔄 3. Update
Call `setState` → trigger re-render
<button onClick={() => setCount(count + 1)}>+1</button>
4 💡 4. Core principle
State is immutable — never mutate directly; always use the setter

🎮 Custom Hooks — Step-by-Step

Click each step to read the content, and track your progress with the ✓ Got it button.
🖥️ Result — rendered React component
✏️ React 코드 수정하기 (클릭해서 열기)
⚛️ React 18 + Babel Standalone — see the result first, then edit the code yourself.

Check Quiz

What naming convention must you follow when creating a custom Hook?
💡 Custom Hooks must start with "use". Thanks to this convention, React can automatically detect violations of the Rules of Hooks via eslint-plugin-react-hooks.
Custom Hooks - React