Skip to main content

Understanding useEffect in ReactJS: A Beginner’s Guide

Photo by Ferenc Almasi on Unsplash


When learning React, one of the most important hooks you’ll encounter is useEffect. It's essential for managing side effects like fetching data, updating the DOM, or setting timers. However, as a beginner, you might run into issues such as unexpected re-renders or infinite loops, which can make it tricky to work with.

In this guide, we’ll break down the whatwherewhen, and why of useEffect, and dive into common mistakes you should avoid. I’ll also provide sample code to help you understand these concepts better.

What is useEffect?

useEffect is a React hook that lets you perform side effects in function components. Side effects include things like:

  • Fetching data from an API
  • Manually interacting with the DOM
  • Subscribing to events (e.g., setting up intervals)
useEffect(() => {
// Side effect logic here
});

The code inside useEffect runs after the component renders, and it can re-run if dependencies change.

Where to use useEffect?

You should place useEffect inside your function component. Here’s an example of fetching data on component mount:

import { useEffect, useState } from 'react';

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

useEffect(() => {
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => setData(data));
}, []); // Empty dependency array, runs once on mount

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

When to use useEffect?

  1. Fetching data from APIs (e.g., when a component mounts)
  2. Setting up timers (e.g., setInterval)
  3. Listening to events (e.g., DOM or WebSocket events)

Why use useEffect?

In React, rendering should be a pure operation (it shouldn’t have side effects). You need useEffect to perform side effects after the component has rendered. For example, fetching data or interacting with the DOM directly should happen in useEffect.

How to use useEffect correctly

1. Run on component mount: Use an empty dependency array to run the effect only once, when the component is first rendered.

useEffect(() => {
console.log('Component mounted');
}, []); // Runs only once

2. Run when a value changes: Include variables in the dependency array to run the effect when those values change.

useEffect(() => {
console.log('Count changed:', count);
}, [count]); // Runs when `count` changes

3. Clean up effects: Use a cleanup function to prevent memory leaks.

useEffect(() => {
const timer = setInterval(() => console.log('Running...'), 1000);
return () => clearInterval(timer); // Cleanup on unmount
}, []); // Runs once, cleanup on unmount

Common Mistakes to Avoid

As a beginner, you might face some issues while working with useEffect. Here are common mistakes and how to avoid them:

1. Missing Dependency Array

If you don’t provide a dependency array, your effect will run after every render.

For example:

useEffect(() => {
console.log('Effect runs on every render');
}); // No dependency array, runs after every render

Problem: This can cause performance issues, and if your effect depends on some state, it can lead to infinite loops.

Solution: Always include a dependency array. If you want to run the effect only once (on mount), pass an empty array [].

useEffect(() => {
console.log('Effect runs only once');
}, []); // Runs only once, on mount

2. Incorrect Dependencies in Array

If you don’t include all the dependencies correctly, React will either not re-run the effect when needed or run it too often.

For example:

useEffect(() => {
console.log('Effect runs when `count` changes');
}, []); // `count` is missing in the dependency array

Problem: The effect depends on count, but it won’t re-run when count changes because it's not included in the dependency array.

Solution: Always list all variables your effect depends on in the dependency array.

useEffect(() => {
console.log('Effect runs when `count` changes');
}, [count]); // Properly includes `count`

3. Unintended Infinite Loops

You may accidentally create an infinite loop if your effect causes a state update, which triggers a re-render, and thus, re-runs the effect.

For example:

useEffect(() => {
setCount(count + 1); // Causes state change
}, [count]); // Effect runs when `count` changes

Problem: The setCount call inside useEffect updates the state, which triggers a re-render, and this re-runs useEffect—causing an infinite loop.

Solution: Ensure that state updates inside useEffect don't trigger unnecessary re-renders. Sometimes you may need to restructure your logic or use conditional updates.

4. Forgetting to Clean Up

When you set up things like event listeners, intervals, or WebSocket connections inside useEffect, you need to clean them up when the component unmounts or before the next effect runs.

For example:

useEffect(() => {
const interval = setInterval(() => console.log('Running...'), 1000);

// Missing cleanup function
}, []); // Runs only once on mount

Problem: If you forget the cleanup, the interval (or other side effects) will keep running even after the component is unmounted, leading to memory leaks.

Solution: Always include a cleanup function.

useEffect(() => {
const interval = setInterval(() => console.log('Running...'), 1000);

return () => clearInterval(interval); // Cleanup on unmount
}, []); // Runs only once on mount

5. Fetching Data in the Wrong Way

Fetching data inside the component body (instead of useEffect) can lead to fetching the same data multiple times, especially on re-renders.

For example:

function DataFetchingComponent() {
fetch('https://api.example.com/data') // Fetching inside component body
.then(response => response.json())
.then(data => console.log(data));

return <div>Loading...</div>;
}

Problem: The fetch will happen every time the component re-renders, even if it’s unnecessary.

Solution: Use useEffect to fetch data on mount:

function DataFetchingComponent() {
useEffect(() => {
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => console.log(data));
}, []); // Fetches once on mount

return <div>Loading...</div>;
}

Conclusion

useEffect is a powerful tool in React, but it can be tricky for beginners. By understanding its usage and avoiding common mistakes like missing dependencies, infinite loops, or forgotten cleanup functions, you can ensure your effects run efficiently and avoid unexpected behaviors.

Key Takeaways:

  • Use the dependency array wisely to control when effects run.
  • Always clean up side effects like intervals and event listeners.
  • Be cautious about unnecessary re-renders and state updates within effects.

With these tips, you’ll be able to handle useEffect confidently and avoid the common pitfalls that many beginners face!

Comments

Popular posts from this blog

Understanding Number Systems: Decimal, Binary, and Hexadecimal

In everyday life, we use numbers all the time, whether for counting, telling time, or handling money. The number system we’re most familiar with is the   decimal system , but computers use other systems, such as   binary   and   hexadecimal . Let’s break down these number systems to understand how they work. What is a Number System? A number system is a way of representing numbers using a set of symbols and rules. The most common number systems are: Decimal (Base 10) Binary (Base 2) Hexadecimal (Base 16) Each system has a different “base” that tells us how many unique digits (symbols) are used to represent numbers. Decimal Number System (Base 10) This is the system we use daily. It has  10 digits , ranging from  0 to 9 . Example: The number  529  in decimal means: 5 × 1⁰² + 2 × 1⁰¹ + 9 × 1⁰⁰ =  500 + 20 + 9 = 529 Each position represents a power of 10, starting from the rightmost digit. Why Base 10? Decimal is base 10 because it has 10 digits...

How to Monetize Your API as an Individual Developer While Hosting on Your Own Server?

In the API economy, cloud services like AWS, Google Cloud, and Azure offer many conveniences, such as scaling and infrastructure management. However, some developers prefer more control and autonomy, opting to host their APIs on personal servers. Whether for cost efficiency, data privacy, or customization, hosting your own API comes with both advantages and challenges. But, even without cloud platforms, there are effective ways to monetize your API. This guide will explore how individual developers can successfully monetize their APIs while hosting them on their own servers. Why Host Your API on Your Own Server? Hosting your own API gives you full control over the infrastructure and potentially lower long-term costs. Here’s why some developers choose this approach: Cost Control : Instead of paying ongoing cloud fees, you may opt for a one-time or lower-cost hosting solution that fits your budget and resource needs. Data Ownership : You have full control over data, which is critical if ...

API Testing with Jest and Supertest: A Step-by-Step Guide

API testing is essential to ensure your endpoints behave as expected across all scenarios. In this guide, we’ll explore how to use Jest and Supertest to test a sample API with various response types, including success, authentication errors, and validation errors. By the end, you’ll understand how to apply these tools to check for different response structures and status codes. 0. Prerequisites: Setting Up Your Environment Before diving into API testing, it’s important to ensure that your development environment is properly set up. Here’s what you need to do: Step 1: Install Node.js and npm Node.js  is a JavaScript runtime that allows you to run JavaScript code on the server side. It comes with  npm  (Node Package Manager), which helps you install and manage packages. Installation Steps: Download and install Node.js from the  official website . To verify the installation, open your terminal and run: node -v npm -v This should display the installed versions of Node.js...

The Weight of Responsibility: A Developer’s Journey to Balance Passion and Reality

For the past several years, Eddie has been on a steady climb in his career as a developer, but recently, he found himself at a crossroads — caught between the weight of his responsibilities and the desire to pursue his true passions. His journey began with a three-month internship as a web developer, which led to nearly four years in an application developer role. After that, he spent almost a year as a systems associate, managing tasks across systems analysis, quality assurance, and business analysis. Eventually, he returned to full-time software development for another two years before transitioning into more complex roles. For over a year, he worked as a multi-role software developer and database administrator before stepping into his current position as a senior software developer, database administrator, and cloud administrator — occasionally handling security tasks as well. Now, with over 8 years of professional experience, he also leads a small team of developers, which has been...

Avoiding Confusion in API Design: The Importance of Clear Responses

In today’s fast-paced software development landscape, APIs play a crucial role in connecting services and enabling functionality. However, poor design choices can lead to confusion and inefficiency for both developers and users. One such choice is the omission of a response body for successful requests, a practice I recently encountered in an enterprise API designed for bill payments. The Case of the No-Response API The API in question serves two main endpoints: one for inquiring about account validity and another for confirming payment. When successful, the API returned a  200 OK  status but no response body. This design choice led to significant confusion during our integration process. Even the internal team who developed the said API struggled to justify this approach, revealing a lack of clarity around the rationale behind it. Pros of This Design Choice While the intention behind this design may have been to streamline responses, several potential benefits can be identifi...