[React Basics] useEffect Hook: Deep Dive into Dependencies and Component Lifecycle

In React, we often focus on rendering UI based on props and state. But real-world apps need more: fetching data from APIs, setting up event listeners, or directly manipulating the DOM. These actions are called side effects because they’re "outside" the pure UI rendering process.

To manage these tasks cleanly and efficiently in functional components, React gives us a powerful tool: the useEffect Hook.

useEffect Hook: Deep Dive into Dependencies and Component Lifecycle

This article will help you master useEffect, from the basics to advanced techniques and common pitfalls.

1. What is useEffect and Why is it Important? 🧠

Imagine your component as an actor on stage. Its main job is to perform (render UI) based on the script (props and state). But between scenes, it needs to do backstage work: change costumes, prepare props, or take direction.

useEffect is where you do that "backstage" work.

Technically, useEffect is a Hook that lets you perform side effects in functional components. It combines the power of class component lifecycle methods like componentDidMount, componentDidUpdate, and componentWillUnmount.

You need useEffect when you want your component to do something:

  • After it first renders (e.g., fetch data from an API).
  • After every re-render (e.g., update the page title based on state).
  • Right before it’s removed from the DOM (e.g., clean up a subscription or event listener).

2. Syntax and How It Works

The basic syntax of useEffect looks like this:

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

function MyComponent() {
  // ...
  useEffect(() => {
    // The callback contains your side effect logic
    console.log('Component rendered!')

    // (Optional) Cleanup function
    return () => {
      console.log('Cleanup before next render or unmount.')
    }
  }, [dependencies]) // Dependency array
  // ...
}

Let’s break down the 3 most important parts:

a. The Callback (The Effect)

This is where you write your side effect code. It could be a fetch call, document.title = ..., or setInterval. This function runs after rendering is complete and the browser has painted the UI, so it won’t block the display.

b. Dependency Array – The Heart of useEffect ❤️

This is the most important and most confusing part. The array determines when your effect will re-run. There are 3 main cases:

Case 1: No dependency array

useEffect(() => {
  // Runs after EVERY render
  console.log('Re-rendered!')
})

The effect runs after the first render and after every re-render, for any reason. This is rarely used because it can cause performance issues or infinite loops if you update state inside the effect.

Case 2: Empty array []

useEffect(() => {
  // Runs ONLY ONCE after the first render
  fetchData()
}, [])

This is perfect for tasks that should only happen once when the component mounts, like fetching initial data. Equivalent to componentDidMount.

Case 3: Array with values [prop, state]

const [userId, setUserId] = useState(1)

useEffect(() => {
  // Runs on first render, and again EVERY TIME userId changes
  fetchUserProfile(userId)
}, [userId])

The effect runs after the first render, and then React watches the values in the array (userId here). If any value changes between renders, the effect runs again. This is the most powerful and common scenario, equivalent to componentDidUpdate.

c. Cleanup Function 🧹

Some side effects need to be "cleaned up" when they’re no longer needed, to avoid memory leaks. For example:

  • If you set up an interval, you need to clearInterval when the component is destroyed.
  • If you add an event listener to window, you need to remove it.

To do this, just return a function from inside your effect. React will automatically call this function:

  1. Before the component unmounts (is removed from the DOM).
  2. Before the effect runs again on the next render (to clean up the previous effect).

Classic example:

useEffect(() => {
  const handleResize = () => console.log('Window resized!')

  // Add event listener
  window.addEventListener('resize', handleResize)
  console.log('Event listener added!')

  // Cleanup function
  return () => {
    window.removeEventListener('resize', handleResize)
    console.log('Event listener removed!')
  }
}, []) // Runs only once

3. Common Pitfalls and Pro Tips ⚠️

Mastering useEffect means knowing how to avoid common mistakes.

Pitfall 1: Infinite Loop

A classic beginner mistake.

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

  // ⚠️ ERROR: Infinite loop!
  useEffect(() => {
    // 1. This effect runs after every render.
    // 2. It updates the 'count' state.
    // 3. Updating state causes a new render.
    // 4. Component re-renders, effect runs again... -> Infinite!
    setCount((prevCount) => prevCount + 1)
  })
  // ...
}

How to fix: Always provide a dependency array! Ask yourself: "When should this effect re-run?" If only once, use []. If when a value changes, put it in the array.

Pitfall 2: Stale State/Props

This happens when you forget to include a value used in the effect in the dependency array.

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

  useEffect(() => {
    const intervalId = setInterval(() => {
      // ⚠️ ERROR: 'count' here is always 0!
      // The function "captures" the value of 'count' at the time it was created (first render).
      // It will never see updated 'count' values.
      console.log(`Count is: ${count}`)
    }, 1000)

    return () => clearInterval(intervalId)
  }, []) // Empty array, so effect never re-runs

  // ...
}

How to fix:

  1. Add count to the dependency array: [count]. This will clean up the old interval and create a new one every time count changes.
  2. Use functional updates: setCount(c => c + 1). This way, you don’t need to access count directly, so you don’t need to put it in the dependency array.

Pro tip: Always enable the ESLint rule react-hooks/exhaustive-deps. It will warn you if you forget a dependency.

Pro Tip 1: Split Effects by Responsibility

Don’t cram all logic into a single useEffect. If you have unrelated tasks, split them up.

// 👎 NOT RECOMMENDED
useEffect(() => {
  fetchData(id)
  document.title = `User ${id}`
}, [id])

// 👍 RECOMMENDED
useEffect(() => {
  fetchData(id)
}, [id])

useEffect(() => {
  document.title = `User ${id}`
}, [id])

This makes your code easier to read, maintain, and follows the Single Responsibility Principle.

Pro Tip 2: Abstract Logic with Custom Hooks

If you find yourself repeating useEffect logic in many components (e.g., API calls, event listeners), wrap it in a Custom Hook.

// Custom hook: useFetch.js
function useFetch(url) {
  const [data, setData] = useState(null)
  useEffect(() => {
    fetch(url)
      .then((res) => res.json())
      .then(setData)
  }, [url])
  return data
}

// Usage in a component
function UserProfile({ userId }) {
  const user = useFetch(`/api/users/${userId}`)
  // ...
}

Conclusion: useEffect Isn’t Complicated

useEffect isn’t a complicated concept, but it requires attention to detail. To truly master it, always remember these 3 essentials:

  1. The Effect function: What should your code do?
  2. Dependency array: When should your code re-run?
  3. Cleanup function: What needs to be cleaned up after your effect?

Once you internalize this mindset, useEffect becomes a powerful tool for building complex, efficient, and memory-leak-free React apps.

Good luck on your React journey!

Related Posts

[React Basics] React Handling Events: Effective and Optimal Approaches

Handling Events is a crucial skill in React. This article will help you understand the syntax, how to pass arguments, and manage state when working with events.

[React Basics] The key Attribute in React: Understand and Use It Effectively

Do you really understand the key attribute in React? Learn its role, how to use it effectively, and real-world examples to make your code cleaner and more optimized.

[React Basics] useState Hook: How to Manage State Effectively in React

A guide to using the useState Hook for managing state in React function components. Learn practical examples, from basics to advanced, to master this essential React Hook.

[React Basics] Stateful vs. Stateless Components: Differences and Usage in React

What are the core differences between Stateful and Stateless Components in React? Learn when and why to use each type to optimize your app’s performance.