DEV Community

Cover image for The Complete Guide to React Hooks (2025)
Rayan Hossain
Rayan Hossain

Posted on

The Complete Guide to React Hooks (2025)

React Hooks have transformed how developers write React applications. From managing state to handling side effects, and even advanced concurrent features in React 18+, Hooks allow developers to write cleaner, reusable, and maintainable code.

This guide covers all React Hooks — beginner, additional, and advanced — with updated examples for 2025, including React and Next.js scenarios. By the end, you'll understand Hooks deeply and be able to implement them in production-ready projects.


📑 Table of Contents

  1. What Are React Hooks?
  2. Why Beginners Should Learn Hooks
  3. Core Hooks
  4. Additional Hooks
  5. Advanced Hooks
  6. Custom Hooks
  7. Common Patterns & Mistakes
  8. Quick Reference Cheatsheet
  9. Conclusion

What Are React Hooks?

React Hooks are functions that let you "hook into" React state and lifecycle features from functional components.

Before Hooks, developers relied heavily on class components for state and lifecycle logic. Hooks allow you to write cleaner, functional components with the same capabilities.


Why Beginners Should Learn Hooks

  • 🚀 Less Boilerplate: No need for classes, constructors, or binding.
  • 🧹 Cleaner Code: Logic is easier to split and reuse.
  • 🔄 Better Reusability: Custom Hooks enable code reuse.
  • 🌍 Industry Standard: Hooks are now the norm in React 18+ apps.

Core Hooks

useState

useState lets you add state to functional components.

import { useState } from "react";

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

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Use cases: counters, toggles, form inputs, UI states.


useEffect

useEffect handles side effects in functional components. Think API calls, subscriptions, or DOM updates.

import { useState, useEffect } from "react";

function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => setSeconds((s) => s + 1), 1000);
    return () => clearInterval(interval);
  }, []);

  return <p>Timer: {seconds}s</p>;
}
Enter fullscreen mode Exit fullscreen mode

Best practices: always clean up side effects to prevent memory leaks.


useContext

useContext provides global state without prop drilling.

import { createContext, useContext } from "react";

const UserContext = createContext();

function App() {
  return (
    <UserContext.Provider value={{ name: "Rayan" }}>
      <Dashboard />
    </UserContext.Provider>
  );
}

function Dashboard() {
  const user = useContext(UserContext);
  return <h1>Welcome, {user.name}</h1>;
}
Enter fullscreen mode Exit fullscreen mode

Use cases: themes, authentication, language settings.


Additional Hooks

useRef

Access DOM elements or store mutable values without causing re-renders.

import { useRef } from "react";

function FocusInput() {
  const inputRef = useRef();

  return (
    <>
      <input ref={inputRef} placeholder="Focus me" />
      <button onClick={() => inputRef.current.focus()}>Focus</button>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

useMemo

Memoizes expensive calculations.

import { useState, useMemo } from "react";

function Factorial({ number }) {
  const factorial = useMemo(() => {
    console.log("Calculating...");
    const calc = (n) => (n <= 0 ? 1 : n * calc(n - 1));
    return calc(number);
  }, [number]);

  return <p>Factorial: {factorial}</p>;
}
Enter fullscreen mode Exit fullscreen mode

useCallback

Memoizes functions to avoid unnecessary re-renders.

import { useState, useCallback } from "react";
import React from "react";

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

  const increment = useCallback(() => setCount((c) => c + 1), []);

  return <Child onClick={increment} />;
}

const Child = React.memo(({ onClick }) => (
  <button onClick={onClick}>Increment</button>
));
Enter fullscreen mode Exit fullscreen mode

useReducer

For complex state logic.

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 (
    <>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
      <button onClick={() => dispatch({ type: "decrement" })}>-</button>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Advanced Hooks

useLayoutEffect

Runs synchronously after DOM updates.

import { useRef, useLayoutEffect } from "react";

function Box() {
  const ref = useRef();

  useLayoutEffect(() => {
    ref.current.style.backgroundColor = "yellow";
  }, []);

  return <div ref={ref}>Box</div>;
}
Enter fullscreen mode Exit fullscreen mode

useImperativeHandle

Expose custom methods to parent components.

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

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

  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current.focus(),
  }));

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

function Parent() {
  const ref = useRef();
  return (
    <>
      <Input ref={ref} />
      <button onClick={() => ref.current.focus()}>Focus</button>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

useId

Generate unique IDs (SSR-friendly).

import { useId } from "react";

function Form() {
  const id = useId();

  return (
    <>
      <label htmlFor={id}>Email:</label>
      <input id={id} type="email" />
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

useTransition & useDeferredValue

Concurrent features in React 18+.

import { useState, useTransition } from "react";

function Search() {
  const [query, setQuery] = useState("");
  const [isPending, startTransition] = useTransition();

  const handleChange = (e) => {
    startTransition(() => setQuery(e.target.value));
  };

  return (
    <>
      <input onChange={handleChange} placeholder="Search..." />
      {isPending && <p>Loading...</p>}
      <p>Results: {query}</p>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

useSyncExternalStore

Subscribe to external stores safely.

import { useSyncExternalStore } from "react";

const store = {
  state: 0,
  listeners: [],
  increment() {
    this.state += 1;
    this.listeners.forEach((l) => l());
  },
  subscribe(listener) {
    this.listeners.push(listener);
    return () => {
      this.listeners = this.listeners.filter(l => l !== listener);
    };
  },
  getSnapshot() {
    return this.state;
  }
};

function Counter() {
  const state = useSyncExternalStore(
    store.subscribe.bind(store),
    store.getSnapshot.bind(store)
  );

  return (
    <>
      <p>Count: {state}</p>
      <button onClick={() => store.increment()}>+</button>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

useInsertionEffect

Inject CSS before layout effects (advanced use, CSS-in-JS).

import { useInsertionEffect } from "react";

function StyledComponent() {
  useInsertionEffect(() => {
    const style = document.createElement("style");
    style.textContent = `p { color: red; }`;
    document.head.appendChild(style);
    return () => document.head.removeChild(style);
  }, []);

  return <p>Styled text</p>;
}
Enter fullscreen mode Exit fullscreen mode

useEvent (Experimental)

Note: useEvent is still experimental and not available in stable React releases as of 2025. The concept is designed for optimized event handlers.

// Experimental - not yet in stable React
import { useEvent } from "react";

function Button() {
  const handleClick = useEvent(() => console.log("Clicked!"));
  return <button onClick={handleClick}>Click Me</button>;
}
Enter fullscreen mode Exit fullscreen mode

Next.js Specific Hooks

useServerInsertedHTML

Insert HTML during server rendering.

import { useServerInsertedHTML } from "next/navigation";

export default function FontStyles() {
  useServerInsertedHTML(() => (
    <style>{`body { font-family: Arial; }`}</style>
  ));

  return null;
}
Enter fullscreen mode Exit fullscreen mode

useOptimistic

For optimistic UI updates in Next.js 13+.

import { useOptimistic } from "react";

function Likes() {
  const [likes, addLike] = useOptimistic(0, (state, delta) => state + delta);

  return (
    <button onClick={() => addLike(1)}>Likes: {likes}</button>
  );
}
Enter fullscreen mode Exit fullscreen mode

Custom Hooks

Custom hooks let you reuse logic.

import { useState, useEffect } from "react";

function useFetch(url) {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetch(url)
      .then(res => res.json())
      .then(setData);
  }, [url]);

  return data;
}

function Posts() {
  const posts = useFetch("https://jsonplaceholder.typicode.com/posts");
  return posts ? <p>{posts.length} posts loaded</p> : <p>Loading...</p>;
}
Enter fullscreen mode Exit fullscreen mode

Common Patterns & Mistakes

  1. Always cleanup side effects (useEffect, useLayoutEffect)
  2. Avoid unnecessary re-renders by memoizing functions (useCallback)
  3. Do not call hooks conditionally - they must be called at the top level
  4. Use custom hooks to reuse logic instead of duplicating code
  5. Combine useReducer and context for global state management

Quick Reference Cheatsheet

Hook Purpose Basic Syntax
useState Component state const [state, setState] = useState(initial)
useEffect Side effects useEffect(() => {}, [deps])
useContext Global state const value = useContext(MyContext)
useRef DOM or mutable value const ref = useRef()
useMemo Memoize value const memo = useMemo(() => compute(), [deps])
useCallback Memoize function const fn = useCallback(() => {}, [deps])
useReducer Complex state [state, dispatch] = useReducer(reducer, init)
useLayoutEffect Synchronous effect useLayoutEffect(() => {}, [deps])
useImperativeHandle Expose methods useImperativeHandle(ref, () => ({}))
useId Unique ID const id = useId()
useTransition Concurrent update [isPending, startTransition] = useTransition()
useDeferredValue Defer value const deferred = useDeferredValue(value)
useSyncExternalStore External store useSyncExternalStore(subscribe, getSnapshot)
useInsertionEffect CSS insertion useInsertionEffect(() => {}, [])
useEvent Event handler (Experimental) const handler = useEvent(() => {})
useServerInsertedHTML SSR HTML (Next.js) useServerInsertedHTML(() => <style>{}</style>)
useOptimistic Optimistic UI (Next.js) [state, mutate] = useOptimistic(initial, reducer)

Conclusion

React Hooks are the modern standard for building React apps. From state management to advanced concurrent features, understanding Hooks will make your code cleaner, faster, and maintainable.

By mastering core, additional, and advanced Hooks, and combining them with custom Hooks, you can confidently build complex React and Next.js applications in 2025.

Top comments (1)

Collapse
 
hashbyt profile image
Hashbyt

Mastering custom hooks and combining them with memoization patterns is key to optimizing React app performance and code reuse critical for fast iteration and seamless UI/UX delivery in complex SaaS products.