Deep Dive into React Hooks:
useState, useEffect, useContext, and Custom Hooks

Last updated: April 13, 2025

1. Introduction to React Hooks

React Hooks, introduced in React 16.8, revolutionized how developers write React components. They allow you to use state and other React features in functional components, eliminating the need for class components in many cases. Hooks make components cleaner, more reusable, and easier to test. This article provides a deep dive into the most essential Hooks: useState, useEffect, useContext, and explores how to create your own custom Hooks.

2. The `useState` Hook

The useState Hook is the most fundamental hook. It allows you to add state variables to functional components.

2.1 Managing Simple State

useState takes an initial state value as an argument and returns an array containing two elements: the current state value and a function to update that value.

import React, { useState } from 'react';

function Counter() {
  // Declare a state variable 'count' initialized to 0
  // 'setCount' is the function to update 'count'
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

export default Counter;

In this example, useState(0) initializes the count state to 0. Clicking the button calls setCount, passing the new value (count + 1), which triggers a re-render with the updated count.

2.2 Functional Updates

If the new state depends on the previous state, it's safer to pass a function to the state updater. This function receives the previous state and returns the new state. This prevents potential issues with stale state closures.

import React, { useState } from 'react';

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

  const incrementThreeTimes = () => {
    // Using functional updates ensures each increment uses the latest state
    setCount(prevCount => prevCount + 1);
    setCount(prevCount => prevCount + 1);
    setCount(prevCount => prevCount + 1);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={incrementThreeTimes}>+3</button>
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  );
}

export default Counter;

Using setCount(prevCount => prevCount + 1) guarantees that each update builds upon the correct previous value, even when updates are batched together.

3. The `useEffect` Hook

The useEffect Hook lets you perform side effects in functional components. Common side effects include data fetching, setting up subscriptions, and manually changing the DOM.

3.1 Handling Side Effects

useEffect runs after every render by default. You pass it a function (the "effect") that contains the imperative code.

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

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

  // useEffect runs after every render
  useEffect(() => {
    // Update the document title using the browser API
    document.title = `You clicked ${count} times`;
    console.log('Document title updated!');
  }); // No dependency array: runs after every render

  return (
    <div>
      <p>You clicked {count} times (Check the document title!) setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

export default DocumentTitleUpdater;

This effect updates the document's title whenever the count state changes (triggering a re-render).

3.2 Cleanup & Dependency Array

Often, effects require cleanup, like unsubscribing from a service or clearing a timer. You can return a function from your effect, which React will run before the component unmounts or before the effect runs again.

To control when an effect runs, you provide a dependency array as the second argument to useEffect. The effect only re-runs if values in the array have changed since the last render. An empty array [] means the effect runs only once after the initial render.

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

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

  useEffect(() => {
    // Start interval
    const intervalId = setInterval(() => {
      setSeconds(prevSeconds => prevSeconds + 1);
    }, 1000);

    console.log('Timer effect started');

    // Cleanup function: runs on unmount or before effect re-runs
    return () => {
      clearInterval(intervalId);
      console.log('Timer effect cleaned up');
    };
  }, []); // Empty dependency array: effect runs only once on mount

  return <div>Timer: {seconds}s</div>;
}

export default Timer;

In this Timer example, the effect sets up an interval on mount and clears it on unmount, thanks to the cleanup function and the empty dependency array [].

If the effect depends on props or state, include them in the dependency array to ensure the effect re-runs when they change. For example useEffect(() => { /* ... */ }, [propA, stateB]);.

4. The `useContext` Hook

The useContext Hook provides a way to pass data through the component tree without having to pass props down manually at every level (prop drilling).

4.1 Avoiding Prop Drilling

Imagine having a theme (e.g., 'light' or 'dark') needed by many components deep in the tree. Passing it down via props is cumbersome. Context provides a cleaner solution.

4.2 Creating and Providing Context

First, create a Context object using React.createContext. Then, use the Context.Provider component higher up the tree to wrap components that need access to the context value. Finally, components that need the data use the useContext hook.

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

// 1. Create Context
const ThemeContext = createContext('light'); // Default value

function App() {
  const [theme, setTheme] = useState('light');

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

  // 2. Provide Context Value
  return (
    <ThemeContext.Provider value={theme}>
      <div className={`app ${theme}`}>
        <h1>Context Example</h1>
        <button onClick={toggleTheme}>Toggle Theme</button>
        <Toolbar />
      </div>
    </ThemeContext.Provider>
  );
}

function Toolbar() {
  // Toolbar doesn't need the theme itself, but contains components that do
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

function ThemedButton() {
  // 3. Consume Context Value
  const theme = useContext(ThemeContext);

  return (
    <button className={`button ${theme}`}>
      I am a {theme} themed button
    </button>
  );
}

export default App;

/* Add basic CSS for themes:
.app.light { background-color: #fff; color: #333; }
.app.dark { background-color: #333; color: #fff; }
.button.light { background-color: #eee; color: #333; border: 1px solid #ccc; }
.button.dark { background-color: #555; color: #fff; border: 1px solid #777; }
*/

useContext(ThemeContext) finds the nearest ThemeContext.Provider above it in the tree and returns its value prop.

5. Building Custom Hooks

Custom Hooks are a powerful mechanism for code reuse in React. They allow you to extract component logic into reusable functions.

5.1 Why Create Custom Hooks?

If you find yourself writing the same stateful logic or side effects in multiple components (e.g., fetching data, subscribing to events, form handling), you can extract that logic into a custom Hook. This promotes cleaner components and better organization.

A custom Hook is simply a JavaScript function whose name starts with "use" and that can call other Hooks (like useState or useEffect).

5.2 Example: A `useFetch` Hook

Let's create a simple custom Hook to handle fetching data from an API.

import { useState, useEffect } from 'react';

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

  useEffect(() => {
    // Prevent race conditions if URL changes quickly
    const abortController = new AbortController();
    const signal = abortController.signal;

    setLoading(true);
    setError(null);

    fetch(url, { signal })
      .then(response => {
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        return response.json();
      })
      .then(fetchedData => {
        setData(fetchedData);
        setError(null);
      })
      .catch(fetchError => {
         if (fetchError.name === 'AbortError') {
           console.log('Fetch aborted');
         } else {
           setError(fetchError);
           setData(null);
         }
      })
      .finally(() => {
        // Only set loading to false if the request wasn't aborted
        if (!signal.aborted) {
           setLoading(false);
        }
      });

    // Cleanup function to abort fetch if component unmounts or URL changes
    return () => {
      abortController.abort();
    };
  }, [url]); // Re-run effect if the URL changes

  return { data, loading, error };
}

export default useFetch;

Now, you can use this Hook in any component:

import React from 'react';
import useFetch from './useFetch'; // Assuming useFetch is in a separate file

function UserProfile({ userId }) {
  const { data: user, loading, error } = useFetch(`https://jsonplaceholder.typicode.com/users/${userId}`);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error fetching data: {error.message}</p>;
  if (!user) return null;

  return (
    <div>
      <h2>{user.name}</h2>
      <p>Email: {user.email}</p>
      <p>Website: {user.website}</p>
    </div>
  );
}

export default UserProfile;

The component UserProfile is now much simpler, focusing only on rendering the UI based on the state provided by the useFetch Hook.

6. Conclusion

React Hooks provide a more direct API to the React concepts you already know: state, lifecycle, context, and refs. They enable writing simpler, more powerful functional components and facilitate logic reuse through custom Hooks. Mastering useState for state management, useEffect for side effects, useContext for accessing shared data, and creating custom Hooks is essential for modern React development.

7. Additional Resources

Related Articles

External Resources