← Back

React.js + Next.js · Module 3. Forms and Effects · Lesson 12 · Point 12

React useEffect Cleanup

React + TypeScript · GoIT Dev Notes

This lesson explains Effect cleanup with useEffect, setInterval, clearInterval, component lifecycle, dependency arrays, StrictMode, React state, timers, and event listeners.

You will learn how React cleans up ongoing external work before an Effect runs again or after a component unmounts, then build a real-time clock that starts and stops safely.


REACT · 01

1. What Is Effect Cleanup?

In React, useEffect() allows us to perform side effects after a component renders.

Examples of side effects include:

  • Making HTTP requests.
  • Starting timers.
  • Setting up intervals.
  • Adding event listeners.
  • Connecting to external services.

Some Effects need to be stopped when a component is removed or before the Effect runs again.

This process is called Effect cleanup.

React allows us to return a function from useEffect(). This function is called the cleanup function.

Basic Example

File: src/App.tsx

import { useEffect } from "react";

export default function App() {
  useEffect(function () {
    console.log("Effect started!");

    // Cleanup function
    return function () {
      console.log("Effect cleaned up!");
    };
  }, []);

  return <h1>Hello React!</h1>;
}

Browser Result

Hello React!

Console after mounting

Effect started!

The Effect starts after the component is committed. React remembers the returned cleanup function for later.

When the component mounts, React runs the Effect.

When the component unmounts, React runs the cleanup function.

With StrictMode disabled, the console displays:

Effect started!

When the component unmounts:

Effect cleaned up!

Important: Returning a function does not execute it immediately. React remembers the function and calls it when cleanup is needed.


REACT · 02

2. Understanding setInterval()

JavaScript provides the setInterval() function to execute code repeatedly at a specified time interval.

Example

function showMessage() {
  console.log("Hello React!");
}

setInterval(showMessage, 1000);

The console displays:

Hello React!
Hello React!
Hello React!
...

A new message appears approximately every second.

How Does It Work?

setInterval(showMessage, 1000);
  • setInterval() starts a repeating timer.
  • showMessage is the function to execute.
  • 1000 represents 1000 milliseconds, or one second.

Common intervals:

Milliseconds Time
1000 1 second
2000 2 seconds
5000 5 seconds
60000 1 minute

Important: setInterval() continues running until it is stopped or its execution environment ends.


REACT · 03

3. Stop an Interval with clearInterval()

The setInterval() function returns an identifier.

We can save this identifier in a variable and use clearInterval() to stop the interval.

Example

function showMessage() {
  console.log("Hello!");
}

// Start the interval
const intervalId = setInterval(showMessage, 1000);

// Stop the interval
clearInterval(intervalId);

Here:

  • intervalId stores the identifier of the interval.
  • clearInterval(intervalId) stops that interval.

In this example, the interval is stopped immediately, before its first scheduled execution.

Remember

setInterval();   // START
clearInterval(); // STOP

When using these functions, we need to pass the appropriate arguments.


REACT · 04

4. Use setInterval Inside useEffect()

We can start an interval when a React component mounts.

File: src/Timer.tsx

import { useEffect } from "react";

export default function Timer() {
  useEffect(function () {
    setInterval(function () {
      console.log("Timer is running!");
    }, 1000);
  }, []);

  return <p>My Timer</p>;
}

Browser Result

My Timer

Console

Timer is running!

Timer is running!

The interval begins after mounting and keeps logging once per second—even if the component is later removed.

What Happens?

  1. React mounts the component.
  2. useEffect() runs.
  3. setInterval() starts.
  4. The interval executes its callback every second.

The empty dependency array:

[]

means the Effect does not rerun because of normal state or prop updates.

What Is the Problem?

If React removes the component, the interval does not automatically stop.

The component disappears, but the interval continues running.

This can waste resources and cause unwanted background work.


REACT · 05

5. Understand Mounting and Unmounting

React components have different stages.

Mounting

Mounting happens when a component is added to the DOM.

Example:

<Timer />

React creates the component and adds it to the page.

Updating

Updating happens when React renders a component again, for example after a state change.

Unmounting

Unmounting happens when React removes a component from the DOM.

For example:

{isOpen && <Timer />}

When isOpen changes from true to false, React removes the Timer component.

Important: Unmounting a component does not automatically stop every external process started by that component.

We must clean up processes such as intervals and event listeners.


REACT · 06

6. Show and Hide a Component

We can use useState() and conditional rendering to control whether a component is visible.

File: src/App.tsx

import { useState } from "react";
import Timer from "./Timer";

export default function App() {
  const [isOpen, setIsOpen] = useState(false);

  function handleClick() {
    setIsOpen(!isOpen);
  }

  return (
    <>
      <button onClick={handleClick}>
        {isOpen ? "Hide timer" : "Show timer"}
      </button>

      {isOpen && <Timer />}
    </>
  );
}

Browser Result

My Timer

When isOpen is true, React mounts Timer. Clicking Hide timer changes the state and unmounts it.

Explanation

const [isOpen, setIsOpen] = useState(false);

Creates a state variable.

  • false means the Timer is hidden.
  • true means the Timer is visible.
setIsOpen(!isOpen);

Changes the Boolean value.

{isOpen && <Timer />}

Renders the Timer only when isOpen is true.

What Happens When We Click?

Action React behavior
Show timer Mounts Timer
Hide timer Unmounts Timer
Show timer again Mounts a new Timer

REACT · 07

7. The Problem: Multiple Intervals Without Cleanup

Consider this incorrect component.

Incorrect code:

import { useEffect } from "react";

export default function Timer() {
  useEffect(function () {
    setInterval(function () {
      console.log("Timer is running!");
    }, 1000);
  }, []);

  return <p>My Timer</p>;
}

Every time the component mounts, a new interval starts.

However, no interval is stopped when the component unmounts.

With StrictMode disabled:

User action Active intervals
Show timer 1
Hide timer 1
Show timer 2
Hide timer 2
Show timer 3

After multiple mountings, several intervals run simultaneously.

Instead of one message per second, we may see multiple messages.

This can cause unnecessary CPU usage and resource consumption.


REACT · 08

8. Fix the Problem with Cleanup

We can fix the problem by returning a cleanup function from useEffect().

Correct code — src/Timer.tsx:

import { useEffect } from "react";

export default function Timer() {
  useEffect(function () {
    // Start the interval
    const intervalId = setInterval(function () {
      console.log("Timer is running!");
    }, 1000);

    // Stop the interval during cleanup
    return function () {
      clearInterval(intervalId);
    };
  }, []);

  return <p>My Timer</p>;
}

Browser Result

My Timer

Active intervals

Visible: 1

Hidden: 0

Cleanup stops the current interval when Timer unmounts, so repeated mounting never accumulates timers.

Step-by-Step Explanation

Step 1 — Start the interval

const intervalId = setInterval(function () {
  console.log("Timer is running!");
}, 1000);

The interval starts and prints a message approximately every second.

Its identifier is stored in intervalId.

Step 2 — Return the cleanup function

return function () {
  clearInterval(intervalId);
};

React remembers this function.

Step 3 — Unmount the component

When the component is removed, React executes the cleanup function.

Step 4 — Stop the interval

clearInterval(intervalId);

The interval stops.

Result

With StrictMode disabled:

User action Active intervals
Show timer 1
Hide timer 0
Show timer 1
Hide timer 0
Show timer 1

The application no longer accumulates unnecessary intervals.


REACT · 09

9. Cleanup and the Dependency Array

Cleanup does not run only when a component unmounts.

It also runs before an Effect executes again because its dependencies have changed.

Consider this example.

File: src/App.tsx

import { useEffect, useState } from "react";

export default function App() {
  const [count, setCount] = useState(0);

  useEffect(function () {
    console.log("Effect started:", count);

    return function () {
      console.log("Cleanup:", count);
    };
  }, [count]);

  function handleClick() {
    setCount(count + 1);
  }

  return (
    <button onClick={handleClick}>
      Count: {count}
    </button>
  );
}

Browser Result

Second click

Cleanup: 1

Effect started: 2

When count changes, React cleans up the previous Effect before starting the next one with the new value.

Understanding [count]

}, [count]);

The Effect runs:

  1. After the component mounts.
  2. Whenever count changes.

Before React executes the new Effect, it cleans up the previous one.

Console Output

With StrictMode disabled:

Effect started: 0

After the first click:

Cleanup: 0
Effect started: 1

After the second click:

Cleanup: 1
Effect started: 2

After the third click:

Cleanup: 2
Effect started: 3

Why Does Cleanup Show the Previous Value?

Each execution of the Effect remembers the count value from that particular render.

For example:

return function () {
  console.log("Cleanup:", count);
};

If the Effect started when count was 0, its cleanup function remembers 0.

When count changes to 1, React first executes the old cleanup function.

That is why the console displays:

Cleanup: 0
Effect started: 1

Important Rule

The order is:

  1. Previous Effect cleanup.
  2. New Effect setup.

React also runs the final cleanup when the component unmounts.


REACT · 10

10. Understand React StrictMode Cleanup Checks

StrictMode helps developers detect potential problems in React components.

In development mode, React performs additional checks.

One important check involves Effects.

Example

import { useEffect } from "react";

export default function Timer() {
  useEffect(function () {
    console.log("START");

    return function () {
      console.log("CLEANUP");
    };
  }, []);

  return <p>Timer</p>;
}

Browser Result

Timer

Development console

START

CLEANUP

START

Root-level StrictMode tests whether the Effect can clean up and start again safely during development.

When the component is inside a root-level StrictMode, the console normally displays this sequence during initial development mounting:

START
CLEANUP
START

Why Does This Happen?

React tests whether the Effect can safely clean up and start again.

The sequence is:

  1. Start the Effect.
  2. Execute its cleanup.
  3. Start the Effect again.

Without Proper Cleanup

If the Effect starts an interval but does not stop it, the test can leave multiple intervals running.

With Proper Cleanup

useEffect(function () {
  const intervalId = setInterval(function () {
    console.log("Hello!");
  }, 1000);

  return function () {
    clearInterval(intervalId);
  };
}, []);

React can clean up the first interval before starting the next one.

Only the current interval remains active.

Important: These additional StrictMode Effect checks happen in development, not in production. They also depend on where StrictMode is enabled in the component tree.


REACT · 11

11. Work with JavaScript Date

JavaScript provides the Date object for working with dates and times.

Create a Date Object

const time = new Date();

This creates a Date object representing the current date and time.

Display the Time

console.log(time.toLocaleTimeString());

Example output:

14:35:20

The exact format depends on the user's locale and browser settings.

Use Date in React

export default function Timer() {
  const time = new Date();

  return (
    <p>
      Current time: {time.toLocaleTimeString()}
    </p>
  );
}

Browser Result

Current time: 14:35:20

The component displays the time captured during its render, but nothing currently asks React to render it again.

This displays the current time when the component renders.

However, the time does not update automatically every second.

For automatic updates, we need React state.


REACT · 12

12. Build a Real-Time Clock with React

Now let's combine everything.

We will use:

  • useState() to store the current time.
  • useEffect() to start the interval.
  • setInterval() to update the time every second.
  • clearInterval() to stop the interval.
  • toLocaleTimeString() to display the time.

Project Structure

src/
├── App.tsx
├── Timer.tsx
└── main.tsx

File: src/Timer.tsx

import { useEffect, useState } from "react";

export default function Timer() {
  // Store the current date and time
  const [time, setTime] = useState(new Date());

  useEffect(function () {
    // Update the time every second
    const intervalId = setInterval(function () {
      setTime(new Date());
    }, 1000);

    // Stop the interval when the component unmounts
    return function () {
      clearInterval(intervalId);
    };
  }, []);

  return (
    <p>
      Current time: {time.toLocaleTimeString()}
    </p>
  );
}

File: src/App.tsx

import { useState } from "react";
import Timer from "./Timer";

export default function App() {
  // Control whether the timer is visible
  const [isOpen, setIsOpen] = useState(false);

  function handleClick() {
    setIsOpen(!isOpen);
  }

  return (
    <div>
      <h1>React Timer</h1>

      <button onClick={handleClick}>
        {isOpen ? "Hide timer" : "Show timer"}
      </button>

      {isOpen && <Timer />}
    </div>
  );
}

Browser Result

React Timer

Current time: 14:35:23

The clock stores a Date in state, updates it every second, and clears its interval when the user hides Timer.

How Does It Work?

Step 1 — Show the timer

The user clicks:

Show timer

React mounts the Timer component.

Step 2 — Initialize state

const [time, setTime] = useState(new Date());

The initial time is saved in React state.

Step 3 — Start the Effect

useEffect(function () {
  // ...
}, []);

React starts the Effect after mounting.

Step 4 — Start the interval

const intervalId = setInterval(function () {
  setTime(new Date());
}, 1000);

Every second, a new Date object is created and stored in state.

Step 5 — React Updates the Screen

Calling:

setTime(new Date());

updates React state and triggers a re-render.

The displayed clock updates:

14:35:20
14:35:21
14:35:22
14:35:23

Step 6 — Hide the timer

The user clicks:

Hide timer

React unmounts the component.

Step 7 — Cleanup

return function () {
  clearInterval(intervalId);
};

The interval stops.

No unnecessary interval remains active.


REACT · 13

13. Clean Up Event Listeners

Timers are not the only Effects that may need cleanup.

Sometimes we add event listeners to global objects such as window or document.

For example, we may want to detect when the browser window changes size.

Incorrect Example

import { useEffect } from "react";

export default function App() {
  useEffect(function () {
    function handleResize() {
      console.log("Window resized!");
    }

    window.addEventListener("resize", handleResize);
  }, []);

  return <h1>Resize the window</h1>;
}

The event listener is added, but it is never removed.

If the component is repeatedly mounted and unmounted, unused listeners may accumulate.

Correct Example

import { useEffect } from "react";

export default function App() {
  useEffect(function () {
    function handleResize() {
      console.log("Window resized!");
    }

    // Add the event listener
    window.addEventListener("resize", handleResize);

    // Remove the event listener during cleanup
    return function () {
      window.removeEventListener("resize", handleResize);
    };
  }, []);

  return <h1>Resize the window</h1>;
}

Browser Result

Resize the window

Console after resizing

Window resized!

The component listens while mounted and removes the same handler function during cleanup.

Explanation

window.addEventListener("resize", handleResize);

Adds a listener that executes handleResize when the browser window is resized.

window.removeEventListener("resize", handleResize);

Removes that listener.

The same function reference must be provided when removing the listener.

Remember

Start Cleanup
setInterval() clearInterval()
setTimeout() clearTimeout()
addEventListener() removeEventListener()

Cleanup is also useful for subscriptions, connections, and other external resources.

Not every Effect requires cleanup. For example, an Effect that only updates the document title may not need one.


REACT · 14

14. Avoid Common Beginner Mistakes

Mistake 1 — Forgetting Cleanup

Incorrect:

useEffect(function () {
  setInterval(function () {
    console.log("Hello!");
  }, 1000);
}, []);

Correct:

useEffect(function () {
  const intervalId = setInterval(function () {
    console.log("Hello!");
  }, 1000);

  return function () {
    clearInterval(intervalId);
  };
}, []);

Mistake 2 — Executing Cleanup Immediately

Incorrect:

useEffect(function () {
  const intervalId = setInterval(function () {
    console.log("Hello!");
  }, 1000);

  clearInterval(intervalId);
}, []);

The interval is stopped during the Effect setup itself.

Correct:

useEffect(function () {
  const intervalId = setInterval(function () {
    console.log("Hello!");
  }, 1000);

  return function () {
    clearInterval(intervalId);
  };
}, []);

React receives a function that it can call later.

Mistake 3 — Creating an Interval During Rendering

Incorrect:

export default function Timer() {
  setInterval(function () {
    console.log("Hello!");
  }, 1000);

  return <p>Timer</p>;
}

Every render creates another interval.

Correct: Start the interval inside useEffect() and provide cleanup.

Mistake 4 — Thinking Cleanup Runs Immediately

Incorrect assumption:

useEffect(function () {
  console.log("START");

  return function () {
    console.log("STOP");
  };
}, []);

It is incorrect to assume that START and STOP always execute immediately one after another.

React stores the cleanup function and calls it when required.

In development StrictMode, React may deliberately run an extra setup-and-cleanup test.


REACT · 15

15. Summary — Key Rules to Remember

  1. useEffect() is used to synchronize a React component with external systems and other side effects.
  2. setInterval() executes a function repeatedly.
  3. clearInterval() stops an interval.
  4. Always store an interval identifier if you need to stop that interval.
  5. An interval does not automatically stop when its React component unmounts.
  6. Return a cleanup function from useEffect() to release resources.
  7. Cleanup runs when the component unmounts.
  8. Cleanup also runs before an Effect executes again because dependencies changed.
  9. With [count], React cleans up the previous Effect before starting the new Effect.
  10. React StrictMode performs extra development checks to help detect missing cleanup.
  11. useState() can store the current time and trigger clock updates.
  12. new Date() creates a Date object representing the current time.
  13. toLocaleTimeString() formats the time as a readable string.
  14. Global event listeners should be removed when they are no longer needed.
  15. Not every Effect needs cleanup, but Effects that start ongoing external work often do.

The Most Important Pattern

useEffect(function () {
  // 1. SETUP
  // Start something

  return function () {
    // 2. CLEANUP
    // Stop or remove what was started
  };
}, []);

The golden rule: If your Effect starts something that continues running, make sure you understand when and how to stop it.

← Back↑ Back to the top