跪拜 Guibai
← Back to the summary

Every React Hook, Catalogued: From useState to Custom Logic Reuse

React Hooks Complete Guide: From Built-in Hooks to Custom Hooks, Master Them All in One Article

React 16.8 introduced Hooks, giving function components all the capabilities of class components. This article will systematically teach you the usage of all built-in Hooks and how to encapsulate your own custom Hooks, making code reuse effortless.


Preface: Why Do We Need Hooks?

Before React 16.8, function components could only receive props and render UI; they couldn't manage state or handle side effects. If you needed those capabilities, you had to write class components.

The emergence of Hooks completely changed this situation. It allows developers to use React's state and other features without writing class components, greatly expanding the capabilities of function components. From then on, the functional programming paradigm gained wider adoption in React.

Core Value of Hooks


1. The Golden Rules of Hooks

Hooks look like ordinary JavaScript functions, but they represent a special type of reusable UI logic with strict restrictions on where they can be called.

Rule 1: Only Call Hooks at the Top Level

Do not call Hooks inside loops, conditionals, nested functions, or try/catch/finally blocks. Always call Hooks at the top level of your React function component, before any return.

// ❌ Wrong: calling inside a conditional
function Bad({ cond }) {
  if (cond) {
    const theme = useContext(ThemeContext); // Forbidden!
  }
}

// ❌ Wrong: calling inside a loop
function Bad() {
  for (let i = 0; i < 10; i++) {
    const theme = useContext(ThemeContext); // Forbidden!
  }
}

// ❌ Wrong: calling after a conditional return
function Bad({ cond }) {
  if (cond) return;
  const theme = useContext(ThemeContext); // Forbidden!
}

// ❌ Wrong: calling inside an event handler
function Bad() {
  function handleClick() {
    const theme = useContext(ThemeContext); // Forbidden!
  }
}

// ❌ Wrong: calling inside a class component
class Bad extends React.Component {
  render() {
    useEffect(() => {}); // Forbidden!
  }
}

// ✅ Correct: calling at the top level of a function component
function Good() {
  const [count, setCount] = useState(0); // ✅
  const theme = useContext(ThemeContext); // ✅
  // ...
}

Why? React relies on the call order of Hooks to correctly preserve the state of each Hook. If Hooks are called conditionally, the order gets disrupted, leading to state mismatches.

Rule 2: Only Call Hooks from React Functions

// ❌ Wrong: calling in an ordinary JS function
function normalFunction() {
  const [count, setCount] = useState(0); // Forbidden!
}

// ✅ Correct: calling in a custom Hook
function useCustomHook() {
  const [count, setCount] = useState(0); // ✅ Custom Hooks can call other Hooks
  return count;
}

Note: Custom Hooks can call other Hooks — that's the entire point of their existence. Because custom Hooks are also only called when the function component renders.


2. Overview of Built-in Hook Categories

React provides over ten built-in Hooks, which can be grouped by functionality:

Category Hooks Purpose
State Hooks useState, useReducer Manage component state
Context Hooks useContext Read and subscribe to context
Ref Hooks useRef, useImperativeHandle Hold information not used for rendering
Effect Hooks useEffect, useLayoutEffect, useInsertionEffect Connect to external systems
Performance Hooks useMemo, useCallback Skip computations and unnecessary re-renders
Other Hooks useTransition, useDeferredValue, useId, useSyncExternalStore, useDebugValue Concurrent rendering, debugging, and other special scenarios

3. State Hooks: State Management

1. useState — The Most Basic State Hook

useState lets you add state to function components. It returns a state value and a function to update that state value.

import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0); // Initial value is 0

  return (
    <div>
      <p>Clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
}

Lazy Initialization: If the initial state requires expensive computation, you can pass a function:

const [state, setState] = useState(() => {
  const expensiveResult = someExpensiveComputation();
  return expensiveResult;
});

2. useReducer — Complex State Logic

When state update logic is complex, involves multiple sub-values, or depends on previous state, useReducer is a better choice than useState.

import { useReducer } from 'react';

const initialState = { count: 0 };

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      return state;
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
    </>
  );
}

4. Context Hook: Passing Data Across Levels

useContext — Escape Prop Drilling

useContext lets a component receive information from an ancestor component without passing it as props through every level.

import { useContext, createContext } from 'react';

const ThemeContext = createContext('light');

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Toolbar() {
  return <Button />;
}

function Button() {
  const theme = useContext(ThemeContext); // Read directly, no prop drilling
  return <button className={theme}>Button</button>;
}

5. Ref Hooks: Holding Data Not Used for Rendering

1. useRef — A "Storage Box" That Doesn't Trigger Renders

Refs let a component hold information not used for rendering, like DOM nodes or timeout IDs. Unlike state, updating a ref does not trigger a component re-render.

import { useRef, useEffect } from 'react';

function TextInput() {
  const inputRef = useRef(null);

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

  return <input ref={inputRef} type="text" />;
}

2. useImperativeHandle — Customize the Ref Exposed to the Parent

useImperativeHandle lets you customize the instance value or methods exposed to the parent component when using refs. This is useful when you want to limit the parent's access to the child component.

import { forwardRef, useImperativeHandle, useRef } from 'react';

const ChildInput = forwardRef((props, ref) => {
  const inputRef = useRef(null);

  // Only expose focus and clear methods to the parent, not the entire DOM node
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    },
    clear: () => {
      inputRef.current.value = '';
    }
  }));

  return <input ref={inputRef} />;
});

function Parent() {
  const childRef = useRef(null);
  return (
    <>
      <ChildInput ref={childRef} />
      <button onClick={() => childRef.current.focus()}>Focus Child Input</button>
    </>
  );
}

6. Effect Hooks: Connecting to External Systems

1. useEffect — The Most Commonly Used Side-Effect Hook

useEffect lets a component connect to and synchronize with external systems. This includes data fetching, subscription management, DOM manipulation, and more.

import { useState, useEffect } from 'react';

function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);

  useEffect(() => {
    // Side effect: connect to chat room
    const connection = createConnection(roomId);
    connection.connect();
    connection.on('message', (msg) => {
      setMessages(prev => [...prev, msg]);
    });

    // Cleanup function: disconnect when component unmounts or roomId changes
    return () => {
      connection.disconnect();
    };
  }, [roomId]); // Dependency array: re-run when roomId changes

  return <div>{/* Render message list */}</div>;
}

Three Cases for the Dependency Array:

Important Reminder: Effects are an "escape hatch" in the React paradigm. If you don't need to interact with an external system, you probably don't need an Effect.

2. useLayoutEffect — Execute Synchronously Before the Browser Paints

useLayoutEffect is similar to useEffect, but it executes synchronously before the browser repaints the screen. It's suitable for measuring layout, preventing flickers, and similar scenarios.

import { useLayoutEffect, useRef, useState } from 'react';

function Tooltip() {
  const ref = useRef(null);
  const [height, setHeight] = useState(0);

  // Measure DOM height before the browser paints to avoid flickering
  useLayoutEffect(() => {
    if (ref.current) {
      setHeight(ref.current.getBoundingClientRect().height);
    }
  }, []);

  return <div ref={ref}>Height is: {height}px</div>;
}

useEffect vs useLayoutEffect: useEffect executes asynchronously and suits most scenarios; useLayoutEffect executes synchronously and suits operations that need to run immediately after layout updates.


7. Performance Hooks: Performance Optimization

1. useMemo — Cache Computed Results

useMemo returns a memoized value, avoiding repeated computation on every render.

import { useMemo, useState } from 'react';

function ExpensiveComponent({ a, b }) {
  const [count, setCount] = useState(0);

  // Only recompute when a or b changes; otherwise use the cached value
  const expensiveResult = useMemo(() => {
    console.log('Running expensive computation...');
    return a * b * 1000; // Simulate complex computation
  }, [a, b]);

  return (
    <div>
      Result: {expensiveResult}
      <button onClick={() => setCount(count + 1)}>Unrelated state: {count}</button>
    </div>
  );
}

2. useCallback — Cache Function References

useCallback returns a memoized callback function, preventing unnecessary re-renders.

import { useCallback, useState } from 'react';

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

  // Only create a new function when dependencies change; otherwise reuse the previous one
  const handleClick = useCallback(() => {
    console.log('Clicked');
  }, []); // Empty dependencies → never changes

  return (
    <>
      <Child onClick={handleClick} />
      <button onClick={() => setCount(count + 1)}>Parent state: {count}</button>
    </>
  );
}

useMemo vs useCallback: useMemo caches values, useCallback caches functions. Both are for performance optimization, but don't overuse them — only use them when there is a genuine performance problem.


8. New Hooks in React 18

React 18 introduced several new Hooks, primarily around concurrent rendering and server-side rendering.

1. useTransition — Mark Non-Urgent Updates

useTransition lets you mark certain state updates as non-urgent. By default, other state updates are considered urgent, and React allows urgent updates (like typing in an input) to interrupt non-urgent updates (like rendering a search results list).

import { useState, useTransition } from 'react';

function SearchPage() {
  const [input, setInput] = useState('');
  const [list, setList] = useState([]);
  const [isPending, startTransition] = useTransition();

  const handleChange = (e) => {
    const value = e.target.value;
    setInput(value); // Urgent update: update the input immediately

    startTransition(() => {
      // Non-urgent update: can be interrupted
      const newList = generateLargeList(value);
      setList(newList);
    });
  };

  return (
    <div>
      <input value={input} onChange={handleChange} />
      {isPending && <span>Loading...</span>}
      <ul>{list.map(item => <li key={item.id}>{item.name}</li>)}</ul>
    </div>
  );
}

2. useDeferredValue — Defer a Non-Urgent Value

useDeferredValue lets you defer re-rendering a non-urgent part of the tree. It's similar to debouncing but has no fixed time delay — React will attempt the deferred render immediately after the first render completes.

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

function SearchPage() {
  const [text, setText] = useState('');
  const deferredText = useDeferredValue(text);

  // Only recompute when deferredText changes, and the computation can be interrupted
  const list = useMemo(() => {
    return generateLargeList(deferredText);
  }, [deferredText]);

  return (
    <div>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <List items={list} />
    </div>
  );
}

3. useId — Generate Unique IDs

useId generates unique IDs on both the client and server while avoiding hydration mismatches. It's primarily used for component libraries that integrate with accessibility APIs requiring unique IDs.

import { useId } from 'react';

function FormField({ label }) {
  const id = useId(); // Each call generates a unique ID
  return (
    <div>
      <label htmlFor={id}>{label}</label>
      <input id={id} type="text" />
    </div>
  );
}

9. Custom Hooks: Encapsulating Reusable Logic

Custom Hooks let you extract component logic into reusable functions. They are React's core mechanism for achieving logic reuse.

Core Rules

  1. Hook names must start with use, followed by an uppercase letter
  2. Custom Hooks can call other Hooks (built-in Hooks or other custom Hooks)
  3. Custom Hooks should only be called when the function component renders

Example 1: useOnlineStatus — Monitor Network Status

Suppose you need to track whether the user is online across multiple components:

// useOnlineStatus.js
import { useState, useEffect } from 'react';

function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(navigator.onLine);

  useEffect(() => {
    function handleOnline() {
      setIsOnline(true);
    }
    function handleOffline() {
      setIsOnline(false);
    }

    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);

    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);

  return isOnline;
}

// Usage
function StatusBar() {
  const isOnline = useOnlineStatus();
  return <h1>{isOnline ? '✅ Online' : '❌ Offline'}</h1>;
}

function SaveButton() {
  const isOnline = useOnlineStatus();
  return (
    <button disabled={!isOnline}>
      {isOnline ? 'Save' : 'Reconnecting...'}
    </button>
  );
}

Example 2: useLocalStorage — Sync with localStorage

// useLocalStorage.js
import { useState } from 'react';

function useLocalStorage(key, initialValue) {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error('Failed to read localStorage:', error);
      return initialValue;
    }
  });

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

  return [storedValue, setValue];
}

// Usage
function UserPreferences() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  const [language, setLanguage] = useLocalStorage('language', 'zh');

  return (
    <div>
      <select value={theme} onChange={(e) => setTheme(e.target.value)}>
        <option value="light">Light</option>
        <option value="dark">Dark</option>
      </select>
      <select value={language} onChange={(e) => setLanguage(e.target.value)}>
        <option value="zh">Chinese</option>
        <option value="en">English</option>
      </select>
    </div>
  );
}

Example 3: useDebounce — Debounce

// useDebounce.js
import { useState, useEffect } from 'react';

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

  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      clearTimeout(timer);
    };
  }, [value, delay]);

  return debouncedValue;
}

// Usage
function SearchInput() {
  const [searchTerm, setSearchTerm] = useState('');
  const debouncedSearch = useDebounce(searchTerm, 500);

  useEffect(() => {
    if (debouncedSearch) {
      // Only initiate a search request after the user stops typing for 500ms
      fetchSearchResults(debouncedSearch);
    }
  }, [debouncedSearch]);

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

Best Practices for Custom Hooks

  1. Stay Focused: Each custom Hook should have a single, specific purpose
  2. Avoid Unnecessary Dependencies: Pay attention to the dependency array to prevent infinite loops
  3. Make Calling Code More Declarative: Good custom Hooks make calling code clearer by constraining their behavior
  4. Return the Data You Need: Hooks can return any value — arrays, objects, single values, or functions
  5. Organize by Domain or Feature: Keep related Hooks together

10. Complete Hook Quick Reference

Hook Category Purpose Key Point
useState State Manage simple state Returns [state, setState]
useReducer State Manage complex state logic Suitable for multiple sub-values or complex updates
useContext Context Read context Replaces prop drilling
useRef Ref Hold values that don't trigger renders .current property is mutable
useImperativeHandle Ref Customize exposed ref Used with forwardRef
useEffect Effect Handle side effects Dependency array controls execution timing
useLayoutEffect Effect Execute side effects synchronously Runs before browser paint
useMemo Performance Cache computed results Avoids repeated computation
useCallback Performance Cache function references Prevents unnecessary child re-renders
useTransition Other Mark non-urgent updates React 18 new feature
useDeferredValue Other Defer non-urgent values React 18 new feature
useId Other Generate unique IDs React 18 new feature
useDebugValue Other Debug custom Hooks Only displayed in React DevTools

Conclusion

React Hooks are one of the most important innovations in the React ecosystem. From useState to useEffect, from useMemo to custom Hooks, this API design makes function components incredibly powerful while keeping code concise and maintainable.

Remember three core principles:

  1. Only call Hooks at the top level — don't put them in loops, conditionals, or nested functions
  2. Only call Hooks from React functions — function components or custom Hooks
  3. Custom Hooks must start with use

When you find yourself repeating the same logic across multiple components, it's time to extract a custom Hook. That's exactly what Hooks were designed for — making logic reuse simple and elegant.

If you found this useful, feel free to like, bookmark, and share so more people can benefit! See you next time 👋