Course Lessons

JAVASCRIPT FOUNDATIONS + AI MINDSET

Back to Course

React State & useEffect Hook

JAVASCRIPT... Lesson 23 of 41 9 min

React State & useEffect Hook

Side Effects, Timers, and Cleanup

By AI Learning Assistant  ·  React  ·  useState  ·  useEffect  ·  Cleanup

🤖 Your Kind AI Learning Assistant

Welcome to Day 23! Today we're mastering useState and useEffect — two of the most important React Hooks. Don't worry if useEffect seems confusing at first. I'll explain it step by step, with simple analogies and practical examples. By the end, you'll understand side effects, cleanup functions, and how to avoid infinite re-renders. Let's go! 🚀

What Are Side Effects?

Anything React Can't Handle Automatically

React is great at rendering UI based on state and props. But what about things that aren't rendering? Things like:

  • Fetching data from an API (getting data from a server)
  • Setting up a timer or interval (like a countdown clock)
  • Subscribing to WebSocket connections (real-time updates)
  • Changing the document title manually
  • Saving data to localStorage
  • Adding event listeners to the window or document

These are called side effects because they happen "outside" React's normal rendering flow. React can't automatically handle them, so it gives us a special Hook: useEffect.

🎯 Side Effects Analogy — A Restaurant

Imagine you're a chef. Your main job is cooking meals (rendering UI). But there are other things you need to do: order ingredients (fetch data), set a timer for the oven (setInterval), wash dishes (cleanup). These are "side effects" — they're not cooking, but they're necessary for the kitchen to run smoothly. React's useEffect is like your assistant who handles all these side effects for you!

The useEffect Hook — Step by Step

Run Code After Render · Dependency Array

useEffect is a Hook that lets you perform side effects in your components. It has a simple structure:

useEffect(() => {
  // This code runs AFTER the component renders
  // This is where you put your side effect code
  
  return () => {
    // OPTIONAL: This is the CLEANUP function
    // Runs before component unmounts or before effect runs again
  };
}, [dependencies]); // Dependency array controls WHEN the effect runs

Three important things to understand:

1. The Effect Function — This is where your side effect code lives. It runs after React has updated the DOM.

2. The Cleanup Function (Optional) — If your effect creates something that needs to be cleaned up (like a timer, subscription, or event listener), you return a cleanup function. React runs this cleanup before the component unmounts OR before the effect runs again.

3. The Dependency Array (Optional) — This tells React when to re-run the effect. There are three patterns:

  • No dependency array: Effect runs after EVERY render (rarely used — can cause performance issues)
  • Empty array []: Effect runs ONLY ONCE after the first render (like componentDidMount)
  • Array with dependencies [state, prop]: Effect runs on first render AND whenever any dependency changes

Build a Live Course Countdown Timer

Practical useEffect Project · Step by Step

Let's build a real project together! We'll create a countdown timer that starts when the user clicks a button and counts down from 30 days. This project will teach you:

  • Multiple useState variables
  • setInterval for timers
  • The useEffect cleanup function to prevent memory leaks
  • Conditional effect execution with dependency arrays

📜 Complete src/App.js — Step by Step Commented

// ============================================
// COUNTDOWN TIMER WITH useEffect
// ============================================

// Import the necessary Hooks from React
import { useState, useEffect } from "react";

function App() {
  
  // ============================================
  // STEP 1: Set up state variables
  // ============================================
  
  // days: How many days are left in the countdown
  const [days, setDays] = useState(30);
  
  // started: Whether the countdown has started
  const [started, setStarted] = useState(false);
  
  // ============================================
  // STEP 2: Set up the useEffect for the timer
  // ============================================
  
  useEffect(() => {
    
    // If the countdown hasn't started OR we're at 0, exit early
    if (!started) return;
    if (days === 0) return;
    
    // Create the timer — runs every 1000ms (1 second)
    const timer = setInterval(() => {
      setDays(prev => prev - 1);
    }, 1000);
    
    // ============================================
    // STEP 3: The CLEANUP function — CRITICAL!
    // ============================================
    // Runs when: component unmounts OR before effect runs again
    // Without this, the timer runs FOREVER!
    return () => {
      clearInterval(timer);
    };
    
  }, [started, days]);
  
  // ============================================
  // STEP 4: Render the UI
  // ============================================
  
  const daysColor = days > 10 ? "green" : "red";
  
  return (
    <div style={{ padding: "40px", textAlign: "center" }}>
      <h1>JavaScript Mastery Countdownh1>
      <h2 style={{ fontSize: "80px", color: daysColor }}>{days}h2>
      <p>days remainingp>
      <button
        onClick={() => setStarted(true)}
        disabled={started}
      >
        {started ? "Countdown in progress..." : "Start Countdown"}
      button>
    div>
  );
}

export default App;

✨ Live Interactive Demo

JavaScript Mastery Countdown

30

days remaining

Click Start to see the countdown in action. This is exactly what your React app will do.

Why Do We Need a Cleanup Function?

Prevent Memory Leaks · Stop Running Processes

The cleanup function is one of the most important — and most misunderstood — parts of useEffect. Without it, your app can develop memory leaks, where old timers, subscriptions, or event listeners keep running even after they're no longer needed, slowly consuming more and more memory until your app crashes.

🎯 What Happens Without Cleanup? — A Scary Story

Imagine you set a timer that runs every second. The user navigates away from the page, but the timer keeps running. Every second, it tries to update a component that no longer exists. This wastes CPU, battery, and memory. Worse, if the user comes back and starts another timer, now you have TWO timers running. Leave and return 100 times? 100 timers running simultaneously! Your app will become slower and slower until it crashes. The cleanup function prevents this by stopping the timer when the component unmounts.

💬 Ask Your Kind AI Assistant

Deep Dive: What is the useEffect Cleanup Function?

🗣️ You Ask the AI:

"What is the useEffect cleanup function in React and when do I need it? Show me 3 real examples of memory leaks it prevents."

🤗 Your Kind AI Assistant Responds:

What is the cleanup function?

The cleanup function is an optional function you can return from your useEffect. React runs this cleanup in two situations:

1. When the component unmounts — The user navigates away, and React needs to clean up any leftover resources.

2. Before the effect runs again — When dependencies change, React runs cleanup from the previous effect before running the new one.

When do you need a cleanup function?

  • setInterval or setTimeout timers
  • Event listeners (window.addEventListener)
  • WebSocket or API subscriptions
  • Animation frames (requestAnimationFrame)
  • Manual DOM manipulations that need reverting

Real Example 1: Timer Memory Leak

// ❌ WITHOUT CLEANUP — MEMORY LEAK!
useEffect(() => {
  setInterval(() => console.log("Still running!"), 1000);
}, []); // Runs FOREVER even after unmount

// ✅ WITH CLEANUP — PROPERLY CLEANED
useEffect(() => {
  const timer = setInterval(() => {
    console.log("Running only while mounted");
  }, 1000);
  return () => clearInterval(timer);
}, []);

Real Example 2: Event Listener Memory Leak

// ❌ WITHOUT CLEANUP — MEMORY LEAK!
useEffect(() => {
  const fn = () => console.log(window.innerWidth);
  window.addEventListener('resize', fn);
}, []); // Listener stays FOREVER

// ✅ WITH CLEANUP — PROPERLY CLEANED
useEffect(() => {
  const fn = () => console.log(window.innerWidth);
  window.addEventListener('resize', fn);
  return () => window.removeEventListener('resize', fn);
}, []);

Real Example 3: WebSocket Subscription Leak

// ❌ WITHOUT CLEANUP — Connection stays open!
useEffect(() => {
  const ws = new WebSocket('wss://api.example.com');
  ws.onmessage = (msg) => console.log(msg.data);
}, []);

// ✅ WITH CLEANUP — PROPERLY CLOSES
useEffect(() => {
  const ws = new WebSocket('wss://api.example.com');
  ws.onmessage = (msg) => console.log(msg.data);
  return () => ws.close();
}, []);

The Golden Rule: If you create something in useEffect that needs to be destroyed or stopped, you MUST provide a cleanup function. Otherwise, you're creating a memory leak that will degrade performance over time. The cleanup function is your insurance policy against slow, crashy apps!

📝 Your Learning Journal

Today I learned that side effects are things React can't handle automatically — like timers, API calls, and event listeners. useEffect is the Hook that manages these side effects. The dependency array controls when effects run: empty array = once on mount, with dependencies = whenever dependencies change. The cleanup function is CRITICAL for preventing memory leaks — it stops timers, removes event listeners, and closes connections when components unmount. I built a countdown timer that actually works! The setInterval combined with cleanup finally makes sense. 🎯

Dependency Array Patterns

Every Render · Once · On Dependency Change

Pattern Syntax When It Runs Common Use Case
No dependency array useEffect(() => {}) After EVERY render Rarely used — usually causes performance issues
Empty array useEffect(() => {}, []) Once after first render Initial data fetch, setting up subscriptions
With dependencies useEffect(() => {}, [count]) First render + on change Save to localStorage, API calls on prop change

⚠️ Common Mistake: Infinite Re-Render Loop

What causes infinite loops? If you put state in the dependency array AND update that same state inside the effect WITHOUT a condition to stop it, you get an infinite loop!

// ❌ THIS CAUSES AN INFINITE LOOP!
useEffect(() => {
  setCount(count + 1); // Updates count → re-render → effect runs again → infinite!
}, [count]);

// ✅ FIX: Add a condition
useEffect(() => {
  if (count < 10) setCount(count + 1);
}, [count]);

// ✅ FIX: Empty array if you only want to run once
useEffect(() => {
  setCount(10);
}, []); // Runs once, sets count to 10, no infinite loop

🤗 Your Kind AI Assistant — Always Here to Help

💬 Ask me anything! "Why is my countdown going negative?" → I'll help you add a condition to stop when days reach 0.

💬 Stuck on an infinite loop? "My component keeps re-rendering forever!" → I'll help you identify which state is causing the loop.

💬 Ready for a challenge? "How would I add a pause and resume button to the countdown?" → I'll guide you through adding more state and conditional timer logic.

You've mastered useState and useEffect! These are the two most important React Hooks. With these, you can build timers, fetch data, handle subscriptions, and much more. Celebrate your progress — you're becoming a React developer! 🌟

AI-Assisted JavaScript Learning · React State & useEffect · Side Effects, Timers, and Cleanup

Complete this lesson

Mark as complete to track your progress