Back to blog

Monday, March 3, 2025

React useEffect Hook and The Right Way to Use It

cover

React is one of the most popular JavaScript libraries for building user interfaces, and understanding its hooks is crucial for mastering the library. One of the most powerful hooks provided by React is useEffect. This hook allows you to perform side effects in functional components, which can seem complex at first but is quite flexible once you grasp its usage.

What is useEffect?

In React, side effects refer to any activity that affects something outside the scope of the function being executed. This includes operations like data fetching, subscriptions, manual DOM manipulation, and more. Prior to hooks, these operations were typically handled in class components lifecycle methods such as componentDidMount, componentDidUpdate, and componentWillUnmount. The useEffect hook brings this functionality to functional components.

import React, { useEffect } from 'react';

function ExampleComponent() {
    useEffect(() => {
        // Code to perform side effect
        console.log('Component has mounted or updated');

        // Optional cleanup mechanism
        return () => {
            console.log('Component will unmount or re-run the effect');
        };
    }, []); // Dependency array

    return <div>Check the console for messages.</div>;
}

Basic Usage

Side Effects Without Cleanup

The simplest way to use useEffect is to perform side effects that don't require any cleanup. Examples include logging information to the console or sending a network request.

useEffect(() => {
    console.log('Component has rendered');
}, []);

In this example, the effect runs only once after the component mounts. The empty array [] passed as the second argument is the dependency array, and it determines when the effect should run. An empty dependency array means the effect runs only once.

Effects With Cleanup

Some effects require cleanup to prevent memory leaks or undesired behavior. This is where the optional cleanup mechanism comes in. For example, when setting up subscriptions, it's important to unsubscribe when the component unmounts.

useEffect(() => {
    const subscription = props.source.subscribe();

    return () => {
        // Cleanup
        subscription.unsubscribe();
    };
}, [props.source]);

Here, the cleanup function returned from useEffect is called when the component unmounts or before the effect runs again (if the dependency array changes).

Dependency Management

The dependency array is a critical part of useEffect. It allows you to control when the effect runs. Here are some key points about the dependency array:

  • No Dependency Array: If you omit the dependency array, the effect will run after every render.

    useEffect(() => {
        // This runs after every render
    });
    
  • Empty Dependency Array ([]): If you provide an empty array, the effect runs only once after the initial render. This is similar to componentDidMount in class components.

    useEffect(() => {
        // Runs only once after the initial render
    }, []);
    
  • Non-empty Dependency Array: The effect will run every time any of the dependencies change.

    useEffect(() => {
        // Runs whenever `stateProp` or `dispatch` change
    }, [stateProp, dispatch]);
    

Examples of useEffect

Let's look at a few practical examples to understand how useEffect can be used.

Example 1: Data Fetching

One of the most common uses of useEffect is to fetch data from an external API.

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

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

    useEffect(() => {
        const fetchData = async () => {
            const response = await fetch('https://api.example.com/data');
            const result = await response.json();
            setData(result);
        };

        fetchData();
    }, []); // Fetch data only once on mount

    return (
        <div>
            {data ? <pre>{JSON.stringify(data, null, 2)}</pre> : 'Loading...'}
        </div>
    );
}

Example 2: Real-Time Updates

Another example is using useEffect to set up and clean up real-time updates, such as listening to WebSocket messages.

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

function RealTimeComponent({ endpoint }) {
    const [messages, setMessages] = useState([]);

    useEffect(() => {
        const socket = new WebSocket(endpoint);

        socket.onmessage = event => {
            setMessages(prevMessages => [...prevMessages, event.data]);
        };

        return () => {
            socket.close(); // Cleanup: close the WebSocket connection
        };
    }, [endpoint]); // Re-run effect if endpoint changes

    return (
        <div>
            <h2>Messages:</h2>
            <ul>
                {messages.map((message, index) => (
                    <li key={index}>{message}</li>
                ))}
            </ul>
        </div>
    );
}

Example 3: Modifying the DOM

You can also use useEffect to interact with the DOM, though it's generally recommended to avoid this when possible and use props and state.

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

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

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

    return (
        <div>
            <input ref={inputRef} type="text" />
        </div>
    );
}

Tips for Using useEffect Effectively

  • Avoid Infinite Loops: Ensure your effects do not unintentionally cause infinite re-renders by updating state or causing side effects that lead to re-rendering.
  • Optimize Performance: Be mindful of the dependencies you include in the array. Only include variables that are necessary for the effect to work correctly.
  • Use Multiple useEffects: It's okay and advisable to use multiple useEffect hooks in a single component. This can help keep related logic together.

Common Mistakes

1. Omitting Dependency Arrays

Omitting the dependency array can lead to effects running more frequently than necessary, which can cause performance issues.

useEffect(() => {
    // This runs after every render
    console.log('Component has rendered');
});

2. Incorrect Dependency Arrays

Including the wrong dependencies can lead to bugs. Make sure to include all variables and functions used inside the effect that change over time and that are part of the component's dependency tree.

useEffect(() => {
    const handleClick = () => {
        console.log(message);
    };

    window.addEventListener('click', handleClick);

    return () => {
        window.removeEventListener('click', handleClick);
    };
}, [message]); // Missing `handleClick` in the dependency array can cause bugs

3. Not Cleaning Up

Failing to clean up after effects can lead to memory leaks or bugs, especially in cases like subscriptions and event listeners.

useEffect(() => {
    const eventSource = new EventSource('/events');

    eventSource.onmessage = event => {
        console.log('New message:', event.data);
    };

    return () => {
        eventSource.close(); // In here, close the EventSource
    };
}, []);

Best Practices

  • Use Dependencies Wisely: Only include the necessary dependencies to avoid triggering effects unnecessarily.
  • Avoid Empty Dependency Arrays When You Need Them: If your effect truly doesn't need any dependencies and should only run once, an empty array is correct.
  • Separate Independent Effects: If an effect updates state or performs a side effect that doesn't need to be re-run every render, separate it into a different effect.

FAQs

  • Q: Should I always use an empty dependency array?

    • A: Not necessarily. Use an empty array when you want the effect to run only once, like setting up a subscription or fetching data. Otherwise, include the necessary dependencies to control when the effect runs.
  • Q: What if I forget to include a dependency?

    • A: Forgetting a dependency can lead to stale closures, where the effect still uses the initial value of the variable. This can cause bugs that are difficult to debug.
  • Q: Can I use async functions directly in useEffect?

    • A: No, you cannot use async functions directly as the primary function inside useEffect. Instead, you can define an async function inside the effect and then call it.

      useEffect(() => {
          const fetchData = async () => {
              const response = await fetch('https://api.example.com/data');
              const result = await response.json();
              setData(result);
          };
      
          fetchData();
      }, []);
      
  • Q: Can I use useEffect without a cleanup function?

    • A: Yes, you can omit the cleanup function if your effect doesn't require any cleanup, such as logging to the console.

Conclusion

The useEffect hook is a fundamental part of functional components in React, enabling you to handle side effects effectively. By understanding how to use dependencies and cleanup functions, you can write more efficient and bug-free code. Always remember to think carefully about the dependencies you include and the cleanup logic you need.

Summary of useEffect Rules

RuleDescription
Use Dependencies WiselyInclude only the necessary dependencies to control when the effect runs.
Cleanup When NecessaryProvide a cleanup function to prevent memory leaks and other issues, especially for subscriptions and event listeners.
Separate EffectsUse multiple useEffect hooks to separate independent effects, improving the readability of your code.

Additional Resources

By following best practices and understanding the internals of useEffect, you'll be able to leverage this powerful feature of React to build robust and performant applications.

Next Steps

Now that you have a solid understanding of useEffect, consider experimenting with it in your projects. Try out different scenarios where you can use side effects, such as fetching data or handling real-time updates. With practice, you'll become more comfortable with useEffect and the broader React ecosystem. Happy coding!

Further Reading

Feel free to explore these resources to deepen your understanding of how useEffect works and how it compares to lifecycle methods in class components.