DEV Community

Cover image for Understanding Core React Hooks with Practical Examples

Understanding Core React Hooks with Practical Examples

“React Hooks turned complex class components into simple, reusable functions.”

Key Takeaways

  • React Hooks replaced class components as the standard way to write React.
  • useState, useEffect, and useContext are used in almost every production React app.
  • Performance hooks like useMemo, useCallback, and useTransition prevent unnecessary re-renders.
  • useReducer is better than useState for complex state logic.
  • Custom hooks allow reusable business logic across components.
  • React 18 introduced concurrent features like useTransition and useDeferredValue.
  • Hooks must follow the Rules of Hooks to avoid bugs and performance issues.

Index

  1. Introduction to React Hooks
  2. Quick Reference - All Hooks
  3. State Management Hooks
  4. Context Hook
  5. Effect Hooks
  6. Ref Hooks
  7. Performance Hooks
  8. React 18 - Transition Hooks
  9. Utility Hooks
  10. React 19 - Form and Optimistic Hooks
  11. Custom Hooks
  12. Rules of Hooks
  13. Stats
  14. Interesting Facts
  15. FAQ's
  16. Conclusion

Introduction to React Hooks

React Hooks were introduced in React 16.8 (released February 2019) and fundamentally transformed how developers build components. Before hooks, building stateful or interactive React components required writing class components with lifecycle methods like componentDidMount, componentDidUpdate, and componentWillUnmount - verbose, often confusing, and difficult to share logic between components.
Hooks change all of that. They let you use state, side effects, context, and other React features directly inside plain JavaScript functions - no class required.

Here is how each major React version expanded the hooks system:

The real power lies not just in knowing hooks, but in knowing when to use them, when not to, and how to combine them effectively.

Quick Reference - All Hooks at a Glance

“Hooks are not just features in React - they are the foundation of modern React development.”

State Management Hooks

These hooks store and manage data inside your component. Start here - you will use them in every React app you build.

1. useState - Local Component State
The most fundamental hook. Use it to store any simple value that should trigger a re-render when it changes - booleans, strings, numbers, arrays, or objects.

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

<button onClick={() => setCount(prev => prev + 1)}>
  Clicked {count} times
</button>
Enter fullscreen mode Exit fullscreen mode

Real-Life Example: Light Switch

import { useState } from 'react';

function LightSwitch() {
  const [isOn, setIsOn] = useState(false);
  return (
    <button onClick={() => setIsOn(!isOn)}>
      {isOn ? 'Light ON' : 'Light OFF'}
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

Common practical uses:

  • Form inputs and controlled components
  • Toggle dark/light theme
  • Loading and error state management
  • Modal open/close state

2. useReducer - Complex State Logic

Think of useReducer as mini-Redux inside a single component. When state logic becomes complex or involves multiple related values that change together, reach for useReducer instead of stacking multiple useStates.

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

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });
  return (
    <>
      <p>{state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

When to use useReducer over useState:

  • State logic involves multiple sub-values
  • The next state depends on the previous one in complex ways
  • Multi-step forms with validation
  • Shopping cart logic with add, remove, and update quantity

Context Hook

Context lets you share data globally across your component tree without passing props through every level (prop drilling).

3. useContext - Global State Without Redux

import { createContext, useContext } from 'react';

const ThemeContext = createContext('light');

function Button() {
  const theme = useContext(ThemeContext);
  return <button className={theme}>Click me</button>;
}

function App() {
  return (
    <ThemeContext.Provider value='dark'>
      <Button />
    </ThemeContext.Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode

Perfect for:

  • Authentication - storing the logged-in user globally
  • Theme management - dark/light mode across the entire app
  • Language and i18n switching
  • App-wide notifications or toast messages For small-to-medium apps, useContext can fully replace Redux. For very large apps with complex state interactions, consider Redux or Zustand.

Effect Hooks

Effect hooks let you run code that interacts with things outside of React - the browser, external APIs, timers, and subscriptions.

4. useEffect - Side Effects After Render

Runs after the component renders. Use it for anything that needs to happen as a consequence of rendering - API calls, subscriptions, DOM updates, and timers.

// Runs once on mount (empty dependency array)
useEffect(() => {
  fetchUsers();
}, []);

// Runs every time 'userId' changes
useEffect(() => {
  fetchUserDetails(userId);
}, [userId]);

// Cleanup example -- WebSocket or timer
useEffect(() => {
  const timer = setInterval(() => tick(), 1000);
  return () => clearInterval(timer); // cleanup on unmount
}, []);
Enter fullscreen mode Exit fullscreen mode

Common uses:

  • API calls on component mount or when data changes
  • WebSocket and real-time subscriptions
  • Timers and intervals
  • Updating the document title
  • Most Common Mistake: A wrong or missing dependency array causes infinite re-renders. Always include every variable used inside the effect in the dependency array.

5. useLayoutEffect - Before the Browser Paints

Identical to useEffect, but fires synchronously after DOM mutations and before the browser paints pixels to screen. Use this when you need to measure or adjust the DOM to prevent a visible flicker.

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

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

  useLayoutEffect(() => {
    const width = boxRef.current.offsetWidth;
    setHeight(width); // make it a perfect square
  }, []);

  return (
    <div ref={boxRef} style={{ width: '200px', height }}>
      Perfect Square
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Use useLayoutEffect when:

  • Measuring DOM dimensions before showing the component
  • Preventing a visible layout flicker or jump
  • Animating elements that need precise initial position data

6. useInsertionEffect - For CSS Library Authors
Runs before layout and painting to inject styles into the document. This is a very low-level hook used internally by CSS-in-JS libraries like Styled-components and Emotion. You will rarely need this in application code.

import { useInsertionEffect } from 'react';

function StyledComponent() {
  useInsertionEffect(() => {
    const style = document.createElement('style');
    style.innerHTML = '.my-class { color: black; }';
    document.head.appendChild(style);
  }, []);

  return <p className='my-class'>Hello</p>;
}
Enter fullscreen mode Exit fullscreen mode

Ref Hooks

Ref hooks let you hold a mutable value that persists between renders without triggering a re-render, and give you a way to directly access DOM nodes.

7. useRef - DOM Access and Persistent Values
Refs are like a box where you can put any value. Changing a ref does NOT cause a re-render. Two common uses: accessing a DOM element directly, or storing a value that needs to persist across renders.

// Use case 1: Access DOM element
const inputRef = useRef(null);

useEffect(() => {
  inputRef.current.focus(); // Focus on mount
}, []);

<input ref={inputRef} />

// Use case 2: Persist value without causing re-render
const renderCount = useRef(0);
renderCount.current += 1;
Enter fullscreen mode Exit fullscreen mode

Practical uses:

  • Programmatically focus input fields
  • Store the previous value of a prop or state
  • Hold a timer ID without triggering re-renders
  • Integrate with non-React third-party DOM libraries

8. useImperativeHandle - Expose Child Methods to Parent
Customises what a parent component can access when it uses a ref on a child. Used together with forwardRef.

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

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

  useImperativeHandle(ref, () => ({
    focusInput() { inputRef.current.focus(); },
    clearInput() { inputRef.current.value = ''; }
  }));

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

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

Performance Hooks

React re-renders components whenever state or props change. These hooks let you control exactly what gets recalculated or recreated, preventing unnecessary work.

9. useMemo - Memoize Expensive Calculations
Caches the result of an expensive calculation and only recomputes it when its dependencies change.

import { useMemo } from 'react';

function ProductList({ products, category }) {
  const filtered = useMemo(() => {
    return products.filter(p => p.category === category);
  }, [products, category]);

  return filtered.map(p => <div key={p.id}>{p.name}</div>);
}
Enter fullscreen mode Exit fullscreen mode

Do not overuse useMemo. It has its own overhead. Only add it when you have measured a real performance problem.

10. useCallback - Memoize Function References
Returns a memoized version of a function that only changes if its dependencies change. Prevents child re-renders caused by a new function reference on every parent render.

import { useCallback } from 'react';

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

  const handleClick = useCallback(() => {
    console.log('Button clicked!');
  }, []); // Same function reference every render

  return <ChildComponent onClick={handleClick} />;
}
Enter fullscreen mode Exit fullscreen mode

React 18 Transition Hooks

React 18 introduced concurrent rendering - the ability to work on multiple state updates simultaneously and prioritise the most important ones.

11. useTransition - Non-Blocking UI Updates
Marks a state update as non-urgent so React can keep the UI responsive while processing it in the background.

import { useState, useTransition } from 'react';

function SearchPage({ allItems }) {
  const [query, setQuery] = useState('');
  const [list, setList]   = useState(allItems);
  const [isPending, startTransition] = useTransition();

  function handleChange(e) {
    setQuery(e.target.value); // Urgent: update input immediately
    startTransition(() => {
      setList(allItems.filter(i => i.includes(e.target.value)));
    });
  }

  return (
    <>
      <input value={query} onChange={handleChange} />
      {isPending && <p>Loading results...</p>}
      {list.map(item => <p key={item}>{item}</p>)}
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

12. useDeferredValue - Delay a Value Update

Accepts a value and returns a deferred version of it. When the value changes rapidly, the deferred version lags slightly, letting the UI stay responsive.

import { useState, useDeferredValue } from 'react';

function SearchList({ items }) {
  const [search, setSearch] = useState('');
  const deferredSearch = useDeferredValue(search);

  const filtered = items.filter(item => item.includes(deferredSearch));

  return (
    <>
      <input value={search} onChange={e => setSearch(e.target.value)} />
      {filtered.map(item => <p key={item}>{item}</p>)}
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Utility Hooks

13. useId - Unique Accessible IDs
Generates a stable unique ID consistent between server and client rendering. Essential for accessibility - correctly linking HTML labels to their inputs.

“Small hooks, powerful logic - that’s the beauty of modern React.”

import { useId } from 'react';

function FormField({ label }) {
  const id = useId();
  return (
    <>
      <label htmlFor={id}>{label}</label>
      <input id={id} />
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

14. useSyncExternalStore - Subscribe to External Data

The correct way to subscribe to external data sources like browser APIs or Redux store.

import { useSyncExternalStore } from 'react';

function subscribe(callback) {
  window.addEventListener('resize', callback);
  return () => window.removeEventListener('resize', callback);
}

function WindowWidth() {
  const width = useSyncExternalStore(subscribe, () => window.innerWidth);
  return <p>Window is {width}px wide</p>;
}
Enter fullscreen mode Exit fullscreen mode

15. useDebugValue - Debug Custom Hooks

Adds a label to a custom hook that appears in React DevTools. Only useful inside custom hooks for debugging - no effect on production behaviour.

import { useState, useDebugValue } from 'react';

function useOnlineStatus() {
  const [isOnline] = useState(true);
  useDebugValue(isOnline ? 'Online' : 'Offline');
  return isOnline;
}
Enter fullscreen mode Exit fullscreen mode

React 19 Form and Optimistic Hooks

React 19 introduces hooks designed around server actions and forms, making form handling and optimistic UI significantly simpler.

16. useFormStatus - Know If a Form Is Submitting
Must be used inside a component that is a child of a form element. Gives access to the parent form's submission state.

'use client';
import { useFormStatus } from 'react-dom';

function SubmitButton() {
  const { pending } = useFormStatus();
  return (
    <button disabled={pending}>
      {pending ? 'Submitting...' : 'Submit'}
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

17. useFormState - Manage Form Validation State

Manages state that comes back from a form action - like validation errors or success messages.

'use client';
import { useFormState } from 'react-dom';

async function validateName(prevState, formData) {
  if (!formData.get('name')) return { error: 'Name is required' };
  return { success: true };
}
function MyForm() {
  const [state, formAction] = useFormState(validateName, {});
  return (
    <form action={formAction}>
      <input name='name' />
      {state.error && <p>{state.error}</p>}
      <button>Submit</button>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

18. useOptimistic - Instant UI Before Server Confirms

Shows an optimistic (assumed-successful) version of the UI immediately before the server has confirmed the operation. Rolls back automatically on failure.

'use client';
import { useOptimistic, useState } from 'react';

function CommentSection() {
  const [comments, setComments] = useState([]);

  const [optimisticComments, addOptimistic] = useOptimistic(
    comments,
    (state, newComment) => [...state, { text: newComment, pending: true }]
  );

  async function addComment(formData) {
    const text = formData.get('comment');
    addOptimistic(text);
    await postCommentToServer(text);
    setComments(prev => [...prev, { text }]);
  }

  return (
    <>
      <form action={addComment}>
        <input name='comment' />
        <button>Add Comment</button>
      </form>
      {optimisticComments.map((c, i) => (
        <p key={i}>{c.text}</p>
      ))}
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Custom Hooks

Custom hooks let you extract and reuse stateful logic across multiple components. They are simply JavaScript functions that start with 'use' and can call other hooks.
If you find yourself copy-pasting the same useEffect and useState pattern across components, it belongs in a custom hook.

“Custom Hooks are where React truly becomes scalable and elegant.”
Example 1: useFetch - Reusable Data Fetching

import { useState, useEffect } from 'react';

function useFetch(url) {
  const [data, setData]       = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError]     = useState(null);

  useEffect(() => {
    setLoading(true);
    fetch(url)
      .then(res => res.json())
      .then(data => { setData(data); setLoading(false); })
      .catch(err => { setError(err); setLoading(false); });
  }, [url]);

  return { data, loading, error };
}

const { data: users, loading } = useFetch('/api/users');

Enter fullscreen mode Exit fullscreen mode

Example 2: useDebounce - Optimised Search Input

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;
}

const debouncedQuery = useDebounce(searchQuery, 300);
useEffect(() => { searchAPI(debouncedQuery); }, [debouncedQuery]);
Enter fullscreen mode Exit fullscreen mode

Example 3: useToggle - Reusable Boolean Toggle

import { useState } from 'react';

function useToggle(initial = false) {
  const [value, setValue] = useState(initial);
  const toggle = () => setValue(prev => !prev);
  return [value, toggle];
}

const [isOpen, toggle]     = useToggle();
const [isDark, toggleDark] = useToggle(false);
Enter fullscreen mode Exit fullscreen mode

Example 4: useAuth - Authentication State

import { useContext } from 'react';

function useAuth() {
  const { user, setUser } = useContext(AuthContext);

  const login = async (credentials) => {
    const data = await loginAPI(credentials);
    setUser(data.user);
    localStorage.setItem('token', data.token);
  };

  const logout = () => {
    setUser(null);
    localStorage.removeItem('token');
  };

  return { user, isAuthenticated: !!user, isAdmin: user?.role === 'admin', login, logout };
}

Enter fullscreen mode Exit fullscreen mode

Rules of Hooks

React relies on the order in which hooks are called being stable between renders. Breaking these rules causes subtle bugs that are very hard to track down.

Stats

Interesting Facts

  • React Hooks were proposed by Dan Abramov and Sebastian Markbåge.
  • Hooks eliminated the need for most class components.
  • Hooks enabled the creation of custom hooks, which allow logic reuse without higher-order components or render props.
  • Libraries like React Query, Zustand, and TanStack Table heavily rely on hooks internally.
  • Hooks follow strict rules because React depends on the order of hook calls to manage component state efficiently.

FAQ's

1. Are class components deprecated?
No. Class components still work in React, but modern React development almost always uses hooks.

2. Can hooks replace Redux?
For small to medium applications, useContext+useReducer can replace Redux.
However, large applications often benefit from dedicated state libraries like Redux, Zustand, or Jotai.

3. Why must hooks follow strict rules?
React internally tracks hook calls based on their order during each render.
Calling hooks conditionally breaks this order and causes unpredictable behavior.

4. Can custom hooks call other hooks?
Yes - that’s the main idea behind them.

Conclusion

React Hooks fundamentally changed how developers build React applications.

Instead of writing complex class components with lifecycle methods, developers can now manage state, side effects, performance optimizations, and global data using small composable functions.
Learning when to use each hook - and when not to - is one of the most important skills for becoming a confident React developer.
Start with the core hooks: useState, useEffect, useContext, and useRef.

Once comfortable, explore advanced hooks like useMemo, useCallback, and React 18’s concurrent features.

And finally, begin writing your own custom hooks - this is where React truly becomes powerful and scalable.

About the Author:Vatsal is a web developer at AddWebSolution. Building web magic with Laravel, PHP, MySQL, Vue.js & more.

Top comments (0)