DevRoadmap
React

React Hooks Complete Guide: useState, useEffect, and Beyond

React Hooks fundamentally changed how React applications are built. This guide takes you through every important Hook with clear explanations and the kinds of examples you'll actually encounter in real codebases.

READ TIME 11 min read
CATEGORY React
Advertisement

Why Hooks Exist

Before Hooks (pre-2019), stateful logic in React required class components — which came with confusing this binding, verbose lifecycle methods, and difficulty reusing stateful logic between components. Hooks let function components do everything class components could do, with cleaner syntax and better composability. The React team no longer recommends class components for new code.

useState: Managing Local State

useState is the most fundamental Hook. It adds state to a function component and returns the current value plus a setter function.

import { useState } from "react";

function Counter() {
  // [currentValue, setterFunction] = useState(initialValue)
  const [count, setCount] = useState(0);
  const [user, setUser] = useState(null);
  const [items, setItems] = useState([]);
  
  return (
    <button onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  );
}

// Updating state based on previous value — always use function form
setCount(prev => prev + 1); // ✅ safe for async updates

// Updating objects — spread the existing state
setUser(prev => ({ ...prev, name: "New Name" }));

// Updating arrays — create new array
setItems(prev => [...prev, newItem]);

useEffect: Side Effects and Lifecycle

useEffect runs code in response to renders. It replaces componentDidMount, componentDidUpdate, and componentWillUnmount from class components.

import { useState, useEffect } from "react";

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // This runs after every render where userId changes
    setLoading(true);
    
    fetch(`/api/users/${userId}`)
      .then(r => r.json())
      .then(data => {
        setUser(data);
        setLoading(false);
      });
    
    // Cleanup function — runs before next effect or on unmount
    return () => {
      // Cancel requests, clear timers, remove listeners
    };
    
  }, [userId]); // Dependency array — effect re-runs when userId changes
}

// Common dependency array patterns:
useEffect(() => { ... });           // ❌ Runs after EVERY render (usually a bug)
useEffect(() => { ... }, []);       // ✅ Runs ONCE on mount only
useEffect(() => { ... }, [id]);     // ✅ Runs when id changes
useEffect(() => { ... }, [a, b]);   // ✅ Runs when a OR b changes

useContext, useRef, and useReducer

useContext — Avoid Prop Drilling

const ThemeContext = createContext("light");

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Page />  {/* Page doesn't need to pass theme down */}
    </ThemeContext.Provider>
  );
}

function DeepChild() {
  const theme = useContext(ThemeContext); // "dark" — no prop needed
}

useRef — Persist Values Without Re-rendering

function SearchInput() {
  const inputRef = useRef(null);
  
  // Access DOM element directly
  const focusInput = () => inputRef.current.focus();
  
  // Store mutable values that don't trigger re-render
  const renderCount = useRef(0);
  renderCount.current += 1; // doesn't cause re-render
  
  return <input ref={inputRef} />;
}

useReducer — Complex State Logic

const initialState = { count: 0, loading: false, error: null };

function reducer(state, action) {
  switch (action.type) {
    case "INCREMENT": return { ...state, count: state.count + 1 };
    case "SET_LOADING": return { ...state, loading: action.payload };
    default: return state;
  }
}

function Component() {
  const [state, dispatch] = useReducer(reducer, initialState);
  dispatch({ type: "INCREMENT" });
}

Custom Hooks: Reusable Logic

Custom Hooks let you extract component logic into reusable functions. A custom hook is any function that starts with use and calls other Hooks.

// Custom hook that fetches data
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    fetch(url)
      .then(r => r.json())
      .then(setData)
      .catch(setError)
      .finally(() => setLoading(false));
  }, [url]);
  
  return { data, loading, error };
}

// Using the custom hook — completely reusable
function UserProfile({ id }) {
  const { data: user, loading, error } = useFetch(`/api/users/${id}`);
  if (loading) return <Spinner />;
  if (error) return <Error />;
  return <div>{user.name}</div>;
}
Hook RulesOnly call Hooks at the top level (never inside loops, conditions, or nested functions). Only call Hooks from React function components or custom Hooks. These rules ensure React can correctly track Hook state across renders.
Advertisement