Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Managing Component Side Effects in React with useEffect

Tech May 18 2

In React applications, certain operations influence state or behavior outside a component's render scope—these are called side effects. Unlike pure functions that deterministically map inputs to outputs without external impact, side-effecting code interacts with systems beyond the function’s local environment.

Recognizing Side Effects

A pure function example:

function sum(x, y) {
  return x + y;
}

Invoking sum(2, 3) always yields 5, leaving no external trace.

Side-effecting code alters external conditions:

let counter = 0;

function increment() {
  counter += 1; // Mutates outer variable → side effect
  return counter;
}

Typical side-effect actions include:

  • Changing global or enclosing-scope variables
  • Mutating object properties
  • Performing network calls (HTTP requests)
  • Setting timers (setInterval, setTimeout)
  • Accessing browser storage (localStorage)
  • Direct DOM manipulation
  • File I/O in Node.js
  • Using non-deterministic functions like Date.now() or Math.random()

Why Isolate Side Effects?

Unmanaged side effects can cause:

  • Memory leaks
  • Duplicate subscriptions or event listeners
  • Orphaned asynchronous tasks
  • Race conditions in data updates
  • Inconsistent UI rendering

React provides the useEffect Hook to encapsulate and control these operations relative to component lifecycle phases.

Problem Without useEffect

Repeated interval setup on each render leads to resource bloat:

import { useState } from 'react';

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

  setInterval(() => {
    setTime(new Date());
  }, 1000);

  return <div>{time.toLocaleString('en-US')}</div>;
}

Here, every render spawns a new interval, rapidly accumulating timers.

Encapsulating Side Effects with useEffect

Scoping the interval prevents duplication:

import { useEffect, useState } from 'react';

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

  useEffect(() => {
    const ticker = setInterval(() => {
      setTime(new Date());
    }, 1000);

    return () => clearInterval(ticker);
  }, []);

  return <div>{time.toLocaleString('en-US')}</div>;
}

An empty dependency array ensures the effect runs once after initial mount. The cleanup function removes the interval when the component unmounts.

Execution Triggers of useEffect

  • No second argument: effect runs after every render.
  • Empty array []: effect runs only on initial mount.
  • Array with values [val1, val2]: effect runs when any listed value changes.

Example with reactive dependency:

import { useEffect, useState } from 'react';

function Doubler() {
  const [value, setValue] = useState(0);
  const [product, setProduct] = useState(0);

  useEffect(() => {
    setProduct(value * 2);
  }, [value]);

  return (
    <>
      <p>Result: {product}</p>
      <button onClick={() => setValue(v => v + 1)}>Increase</button>
    </>
  );
}

Changing value triggers recalculation of product.

Cleaning Up Effects

Returning a function from useEffect allows cleanup before the next run or upon unmount.

Cleanup on unmount only:

import { useEffect } from 'react';

function TimerPrinter() {
  useEffect(() => {
    const id = setInterval(() => {
      console.log('Tick:', new Date().toLocaleTimeString());
    }, 1000);

    return () => {
      clearInterval(id);
      console.log('Timer stopped');
    };
  }, []);

  return <div>Check console for tick logs.</div>;
}

Cleanup on dependency change:

import { useState, useEffect } from 'react';

function UserFetcher({ uid }) {
  const [info, setInfo] = useState(null);

  useEffect(() => {
    const ctrl = new AbortController();

    fetch(`/users/${uid}`, { signal: ctrl.signal })
      .then(res => res.json())
      .then(setInfo)
      .catch(err => {
        if (err.name !== 'AbortError') console.error(err);
      });

    return () => ctrl.abort();
  }, [uid]);

  return <div>User: {info ? info.username : 'Loading...'}</div>;
}

Switching uid aborts the previous request before starting a new one.

Handling Async Operations Inside useEffect

The useEffect callback cannot be async because it must return either nothing or a synchronous cleanup function, not a Promise.

Correct pattern: define and invoke an async function inside the effect.

import { useEffect, useState } from 'react';

function ProfileView({ userId }) {
  const [profile, setProfile] = useState(null);
  const [errMsg, setErrMsg] = useState('');

  useEffect(() => {
    async function retrieveProfile() {
      try {
        const resp = await fetch(`/profiles/${userId}`);
        if (!resp.ok) throw new Error('Failed to fetch');
        const data = await resp.json();
        setProfile(data);
      } catch (e) {
        setErrMsg(e.message);
      }
    }

    retrieveProfile();
  }, [userId]);

  if (errMsg) return <div>Error: {errMsg}</div>;
  if (!profile) return <div>Loading profile...</div>;
  return <div>Name: {profile.fullName}</div>;
}

For cancellation support, combine AbortController with fetch.

Using an IIFE variant:

useEffect(() => {
  (async () => {
    const resp = await fetch(`/profiles/${userId}`);
    const data = await resp.json();
    setProfile(data);
  })();
}, [userId]);

Abstracting Async Logic into Custom Hooks

Encapsulate repetitive fetch patterns:

// useDataFetch.js
import { useState, useEffect } from 'react';

export function useDataFetch(fetchUrl) {
  const [result, setResult] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const [fetchError, setFetchError] = useState(null);

  useEffect(() => {
    const ctrl = new AbortController();

    async function load() {
      setIsLoading(true);
      try {
        const resp = await fetch(fetchUrl, { signal: ctrl.signal });
        if (!resp.ok) throw new Error('Network response was not ok');
        const json = await resp.json();
        setResult(json);
      } catch (e) {
        if (e.name !== 'AbortError') setFetchError(e);
      } finally {
        setIsLoading(false);
      }
    }

    load();

    return () => ctrl.abort();
  }, [fetchUrl]);

  return { result, isLoading, fetchError };
}

Usage:

import { useDataFetch } from './useDataFetch';

function Page({ pageId }) {
  const { result, isLoading, fetchError } = useDataFetch(`/pages/${pageId}`);

  if (isLoading) return <div>Fetching...</div>;
  if (fetchError) return <div>Oops: {fetchError.message}</div>;
  return <article>{result.content}</article>;
}

Coordinating Multiple Async Steps

Chain sequential fetches within one effect:

useEffect(() => {
  const ctrl = new AbortController();

  async function gatherData() {
    try {
      const tokenResp = await fetch('/auth/token', { signal: ctrl.signal });
      const { accessToken } = await tokenResp.json();

      const dataResp = await fetch(`/secure/data?token=${accessToken}`, { signal: ctrl.signal });
      const payload = await dataResp.json();
      setPayloadData(payload);
    } catch (e) {
      if (e.name !== 'AbortError') setLoadErr(e);
    }
  }

  gatherData();

  return () => ctrl.abort();
}, [someDep]);

Alternatively, use promice chains instead of async/await:

useEffect(() => {
  const ctrl = new AbortController();

  fetch(`/data/${itemId}`, { signal: ctrl.signal })
    .then(r => {
      if (!r.ok) throw new Error('Bad status');
      return r.json();
    })
    .then(setItemData)
    .catch(e => {
      if (e.name !== 'AbortError') setError(e);
    });

  return () => ctrl.abort();
}, [itemId]);

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.