Understanding React Hooks: A Comprehensive Guide with Examples

R
R.S. Chauhan
6/13/2025 10 min read

React Hooks, introduced in React 16.8, revolutionized how developers manage state and side effects in functional components. They allow you to "hook into" React features like state and lifecycle methods without writing class components. This blog post explores React Hooks in depth, covering all major hooks with practical, original examples to help you understand their usage.


What Are React Hooks?

Hooks are functions that let you use state and other React features in functional components. Before hooks, state and lifecycle methods were only available in class components. Hooks make code more reusable, readable, and easier to maintain by allowing logic to be organized into smaller, composable functions.

Why Use Hooks?

  • Simpler Syntax: No need for class components or this keyword.
  • Reusable Logic: Custom hooks let you share logic across components.
  • Cleaner Code: Hooks reduce boilerplate and make components easier to test.
  • Functional Paradigm: Aligns with modern JavaScript’s functional programming trends.

Core React Hooks

Let’s dive into the core hooks provided by React, with examples for each.

1. useState

The useState hook adds state to functional components. It returns an array with the current state and a function to update it.

Example: Counter App
A simple counter that increments or decrements a number.

import React, { useState } from 'react';

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

  return (
    <div>
      <h2>Counter: {count}</h2>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setCount(count - 1)}>Decrement</button>
    </div>
  );
}

Explanation:

  • useState(0) initializes count to 0.
  • setCount updates the state and triggers a re-render.
  • The component re-renders whenever count changes.

Advanced useState Example: Form Input
Managing form input state with an object.

import React, { useState } from 'react';

function UserForm() {
  const [form, setForm] = useState({ name: '', email: '' });

  const handleChange = (e) => {
    setForm({ ...form, [e.target.name]: e.target.value });
  };

  return (
    <div>
      <input
        type="text"
        name="name"
        value={form.name}
        onChange={handleChange}
        placeholder="Enter name"
      />
      <input
        type="email"
        name="email"
        value={form.email}
        onChange={handleChange}
        placeholder="Enter email"
      />
      <p>Name: {form.name}</p>
      <p>Email: {form.email}</p>
    </div>
  );
}

Explanation:

  • useState holds an object with name and email.
  • The spread operator (...form) preserves existing state while updating specific fields.

2. useEffect

The useEffect hook handles side effects, such as fetching data, subscriptions, or DOM manipulation. It runs after every render by default but can be controlled with a dependency array.

Example: Fetching Data
Fetch a list of users from a mock API.

import React, { useState, useEffect } from 'react';

function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch('https://jsonplaceholder.typicode.com/users')
      .then((response) => response.json())
      .then((data) => {
        setUsers(data);
        setLoading(false);
      });
  }, []); // Empty dependency array: runs once on mount

  if (loading) return <p>Loading...</p>;

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

Explanation:

  • useEffect runs the fetch call when the component mounts.
  • The empty dependency array ([]) ensures it runs only once.
  • setUsers updates the state with fetched data, and setLoading toggles the loading state.

Advanced useEffect Example: Window Resize Listener
Track the browser window’s width.

import React, { useState, useEffect } from 'react';

function WindowSize() {
  const [windowWidth, setWindowWidth] = useState(window.innerWidth);

  useEffect(() => {
    const handleResize = () => setWindowWidth(window.innerWidth);
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize); // Cleanup
  }, []); // Empty dependency array

  return <p>Window Width: {windowWidth}px</p>;
}

Explanation:

  • useEffect adds an event listener for window resizing.
  • The cleanup function (returned by useEffect) removes the listener to prevent memory leaks.
  • The dependency array ensures the effect runs only on mount and unmount.

3. useContext

The useContext hook accesses React’s context API, allowing you to share data without prop drilling.

Example: Theme Switcher
Toggle between light and dark themes using context.

import React, { useContext, useState } from 'react';

// Create Context
const ThemeContext = React.createContext();

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  const toggleTheme = () => {
    setTheme(theme === 'light' ? 'dark' : 'light');
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

function ThemeComponent() {
  const { theme, toggleTheme } = useContext(ThemeContext);

  return (
    <div style={{ background: theme === 'light' ? '#fff' : '#333', color: theme === 'light' ? '#000' : '#fff' }}>
      <p>Current Theme: {theme}</p>
      <button onClick={toggleTheme}>Toggle Theme</button>
    </div>
  );
}

function App() {
  return (
    <ThemeProvider>
      <ThemeComponent />
    </ThemeProvider>
  );
}

Explanation:

  • ThemeContext holds the theme state and toggleTheme function.
  • useContext(ThemeContext) provides access to the context values in ThemeComponent.
  • The component dynamically updates styles based on the theme.

4. useReducer

The useReducer hook manages complex state logic, similar to Redux. It’s useful when state transitions involve multiple actions or conditions.

Example: Todo List
A simple todo list with add and remove functionality.

import React, { useReducer } from 'react';

const initialState = { todos: [] };

function reducer(state, action) {
  switch (action.type) {
    case 'ADD_TODO':
      return { todos: [...state.todos, action.payload] };
    case 'REMOVE_TODO':
      return { todos: state.todos.filter((_, index) => index !== action.payload) };
    default:
      return state;
  }
}

function TodoList() {
  const [state, dispatch] = useReducer(reducer, initialState);
  const [input, setInput] = useState('');

  const addTodo = () => {
    if (input.trim()) {
      dispatch({ type: 'ADD_TODO', payload: input });
      setInput('');
    }
  };

  return (
    <div>
      <input
        type="text"
        value={input}
        onChange={(e) => setInput(e.target.value)}
        placeholder="Add a todo"
      />
      <button onClick={addTodo}>Add</button>
      <ul>
        {state.todos.map((todo, index) => (
          <li key={index}>
            {todo} <button onClick={() => dispatch({ type: 'REMOVE_TODO', payload: index })}>Remove</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

Explanation:

  • useReducer manages the todos array with a reducer function.
  • Actions (ADD_TODO, REMOVE_TODO) define state changes.
  • dispatch triggers state updates based on action types.

5. useCallback

The useCallback hook memoizes functions to prevent unnecessary re-creations, improving performance in scenarios involving child components.

Example: Memoized Callback
Pass a memoized function to a child component.

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

function Button({ onClick, label }) {
  console.log(`${label} Button rendered`);
  return <button onClick={onClick}>{label}</button>;
}

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

  const increment = useCallback(() => {
    setCount((prev) => prev + 1);
  }, []); // Empty dependency array: memoized forever

  return (
    <div>
      <p>Count: {count}</p>
      <Button onClick={increment} label="Increment" />
    </div>
  );
}

Explanation:

  • useCallback memoizes the increment function.
  • Without useCallback, the Button component would re-render unnecessarily on every state change.
  • The dependency array ([]) ensures the function is only created once.

6. useMemo

The useMemo hook memoizes expensive computations, re-running them only when dependencies change.

Example: Expensive Calculation
Calculate the sum of numbers in a range.

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

function SumCalculator() {
  const [range, setRange] = useState(1000);
  const [otherState, setOtherState] = useState(0);

  const sum = useMemo(() => {
    console.log('Calculating sum...');
    let total = 0;
    for (let i = 1; i <= range; i++) {
      total += i;
    }
    return total;
  }, [range]); // Only recalculate if range changes

  return (
    <div>
      <p>Sum of numbers up to {range}: {sum}</p>
      <input
        type="number"
        value={range}
        onChange={(e) => setRange(Number(e.target.value))}
      />
      <button onClick={() => setOtherState(otherState + 1)}>
        Other State: {otherState}
      </button>
    </div>
  );
}

Explanation:

  • useMemo caches the sum calculation.
  • The calculation only runs when range changes, not when otherState updates.
  • This improves performance for computationally expensive operations.

7. useRef

The useRef hook creates a mutable reference that persists across renders. It’s commonly used for DOM access or storing values without triggering re-renders.

Example: Focus Input
Focus an input field on button click.

import React, { useRef } from 'react';

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

  const handleClick = () => {
    inputRef.current.focus();
  };

  return (
    <div>
      <input type="text" ref={inputRef} placeholder="Type here" />
      <button onClick={handleClick}>Focus Input</button>
    </div>
  );
}

Explanation:

  • useRef creates a reference to the input element.
  • inputRef.current accesses the DOM node to call focus().

Advanced useRef Example: Tracking Previous State
Track the previous value of a state.

import React, { useState, useRef, useEffect } from 'react';

function PreviousValue() {
  const [count, setCount] = useState(0);
  const prevCountRef = useRef();

  useEffect(() => {
    prevCountRef.current = count; // Update ref after each render
  }, [count]);

  return (
    <div>
      <p>Current Count: {count}</p>
      <p>Previous Count: {prevCountRef.current}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

Explanation:

  • useRef stores the previous count value.
  • useEffect updates the ref after each render, preserving the previous state.

8. useLayoutEffect

The useLayoutEffect hook is similar to useEffect but runs synchronously after DOM updates, before the browser paints. It’s useful for DOM measurements.

Example: Measuring Element Size
Measure the dimensions of a div.

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

function ElementSize() {
  const [size, setSize] = useState({ width: 0, height: 0 });
  const divRef = useRef(null);

  useLayoutEffect(() => {
    const rect = divRef.current.getBoundingClientRect();
    setSize({ width: rect.width, height: rect.height });
  }, []);

  return (
    <div>
      <div ref={divRef} style={{ width: '200px', height: '100px', background: 'lightblue' }}>
        Measure me!
      </div>
      <p>Width: {size.width}px, Height: {size.height}px</p>
    </div>
  );
}

Explanation:

  • useLayoutEffect measures the div’s size immediately after DOM updates.
  • It’s synchronous, ensuring accurate measurements before rendering.

9. useImperativeHandle

The useImperativeHandle hook customizes the instance value exposed to parent components when using ref. It’s used with forwardRef.

Example: Custom Input Focus
Expose a custom focus method to the parent.

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

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

  useImperativeHandle(ref, () => ({
    customFocus: () => {
      inputRef.current.focus();
      inputRef.current.value = 'Custom Focus!';
    },
  }));

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

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

  return (
    <div>
      <CustomInput ref={inputRef} />
      <button onClick={() => inputRef.current.customFocus()}>
        Trigger Custom Focus
      </button>
    </div>
  );
}

Explanation:

  • useImperativeHandle exposes a customFocus method to the parent.
  • The parent can call customFocus via the ref, which focuses the input and sets its value.

10. useDebugValue

The useDebugValue hook labels custom hooks in React DevTools, improving debugging.

Example: Custom Hook with Debug Value
A custom hook to track window size.

import React, { useState, useEffect, useDebugValue } from 'react';

function useWindowSize() {
  const [size, setSize] = useState({ width: window.innerWidth, height: window.innerHeight });

  useEffect(() => {
    const handleResize = () => setSize({ width: window.innerWidth, height: window.innerHeight });
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  useDebugValue(`Width: ${size.width}, Height: ${size.height}`);

  return size;
}

function WindowSizeDisplay() {
  const { width, height } = useWindowSize();

  return (
    <p>
      Window Size: {width}x{height}
    </p>
  );
}

Explanation:

  • useDebugValue displays the window size in React DevTools for the useWindowSize hook.
  • This helps developers debug custom hooks.

Custom Hooks

Custom hooks are reusable functions that encapsulate logic using built-in hooks. They follow the naming convention useSomething.

Example: useToggle
A custom hook to toggle a boolean state.

import React, { useState } from 'react';

function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue);

  const toggle = () => setValue(!value);

  return [value, toggle];
}

function ToggleComponent() {
  const [isOn, toggle] = useToggle(false);

  return (
    <div>
      <p>Toggle is {isOn ? 'ON' : 'OFF'}</p>
      <button onClick={toggle}>Toggle</button>
    </div>
  );
}

Explanation:

  • useToggle encapsulates toggle logic using useState.
  • It returns the current state and a toggle function, reusable across components.

Rules of Hooks

To ensure hooks work correctly, follow these rules:

  1. Only Call Hooks at the Top Level: Don’t call hooks inside loops, conditions, or nested functions.
  2. Only Call Hooks from React Functions: Use hooks in functional components or custom hooks, not regular JavaScript functions.
  3. Use the ESLint plugin eslint-plugin-react-hooks to enforce these rules.

Example of Incorrect Usage:

function BadComponent() {
  if (true) {
    const [state, setState] = useState(0); // Error: Hook in condition
  }
}

Correct Usage:

function GoodComponent() {
  const [state, setState] = useState(0); // Top-level hook
}

Best Practices

  • Keep Hooks Simple: Break complex logic into custom hooks.
  • Use Dependency Arrays Wisely: Specify all dependencies in useEffect and useMemo to avoid bugs.
  • Clean Up Effects: Return cleanup functions in useEffect to prevent memory leaks.
  • Memoize When Necessary: Use useCallback and useMemo only when performance is an issue.
  • Debug with useDebugValue: Label custom hooks for better debugging.

Conclusion

React Hooks simplify state management and side effects in functional components, making React code more modular and maintainable. By mastering hooks like useState, useEffect, useContext, and others, you can build efficient, reusable components. Custom hooks take this further by encapsulating logic for reuse across your application.

Try experimenting with the examples provided to deepen your understanding. Happy coding!

Web DevelopmentJavaScriptreactjavascriptweb developmentReact

Related Quizzes

JavaScript Variable

In JavaScript, a variable is a container that stores data values. Think of it like a box that you can put different things in. These "things" could be numbers, text, or even more complex information.

Comments (0)

No comments yet. Be the first to comment!