← Back to Course Overview

Module 3 - Composing Side Effects

Understanding Side Effects

Learn what side effects are in React components and when to use them.

In React, side effects are operations that affect something outside the scope of the current function. These include data fetching, subscriptions, manual DOM manipulations, logging, and other operations that don't directly relate to rendering but are necessary for your application to work correctly.

Examples of side effects in React components include:

React provides the useEffect hook specifically for handling side effects in functional components. This hook lets you perform side effects in a controlled and predictable way.

Working with Side Effects

Master the techniques of working with side effects in React components.

The useEffect hook is the primary way to implement side effects in React functional components. It takes two arguments: a function that contains the side effect code, and an optional dependency array.

The basic syntax of useEffect is:

import React, { useEffect } from 'react';

function MyComponent() {
  useEffect(() => {
    // Side effect code goes here
    
    // Optional cleanup function
    return () => {
      // Cleanup code goes here
    };
  }, [/* dependency array */]);
  
  return (
    // JSX for rendering
  );
}

The dependency array determines when the effect runs:

Triggering Side Effects

Learn how to trigger side effects based on state and prop changes.

One of the most powerful features of useEffect is the ability to selectively run side effects when specific values change. This is achieved through the dependency array, which tells React to only re-run the effect when the listed values have changed.

Here's an example where an effect runs whenever a search query changes:

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

function SearchResults() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  
  useEffect(() => {
    // Skip the effect on first render with empty query
    if (query === '') return;
    
    console.log(`Searching for: ${query}`);
    
    // Fetch search results based on query
    const fetchResults = async () => {
      try {
        const response = await fetch(`https://api.example.com/search?q=${query}`);
        const data = await response.json();
        setResults(data.results);
      } catch (error) {
        console.error('Error fetching results:', error);
      }
    };
    
    fetchResults();
  }, [query]); // Only re-run when query changes
  
  return (
    <div>
      <input 
        type="text" 
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search..."
      />
      <ul>
        {results.map(item => (
          <li key={item.id}>{item.title}</li>
        ))}
      </ul>
    </div>
  );
}

In this example, the useEffect hook runs whenever the query state changes. This creates a reactive search feature where the results update as the user types.

Fetching Data on Component Mount

Learn how to fetch data when a component mounts using side effects.

A common use case for useEffect is fetching data when a component first mounts. This is done by using an empty dependency array, which ensures the effect only runs once after the initial render.

Here's an example of fetching data on component mount:

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

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    // Function to fetch user data
    const fetchUser = async () => {
      try {
        setLoading(true);
        const response = await fetch(`https://api.example.com/users/${userId}`);
        
        if (!response.ok) {
          throw new Error('Failed to fetch user data');
        }
        
        const userData = await response.json();
        setUser(userData);
        setError(null);
      } catch (err) {
        setError(err.message);
        setUser(null);
      } finally {
        setLoading(false);
      }
    };
    
    fetchUser();
  }, [userId]); // Re-fetch when userId changes
  
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  if (!user) return <div>No user data found</div>;
  
  return (
    <div className="user-profile">
      <h2>{user.name}</h2>
      <p>Email: {user.email}</p>
      <p>Username: {user.username}</p>
      <p>Phone: {user.phone}</p>
    </div>
  );
}

This pattern is incredibly common in React applications. It handles three important states: loading, error, and success, providing a good user experience regardless of the API response.

Cleaning Up Side Effects

Learn how to clean up side effects when a component unmounts.

Some side effects need to be cleaned up when a component unmounts to prevent memory leaks or unexpected behavior. Examples include canceling network requests, clearing timers, or removing event listeners.

In useEffect, you can return a cleanup function which React will call when the component unmounts or before the effect runs again (if the dependencies change).

Here's an example with a timer that gets properly cleaned up:

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

function Timer() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    console.log('Setting up timer...');
    const intervalId = setInterval(() => {
      setCount(prevCount => prevCount + 1);
    }, 1000);
    
    // Cleanup function
    return () => {
      console.log('Cleaning up timer...');
      clearInterval(intervalId);
    };
  }, []); // Empty dependency array means this runs once on mount
  
  return <div>Counter: {count}</div>;
}

Another common example is removing event listeners:

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

function WindowSize() {
  const [windowWidth, setWindowWidth] = useState(window.innerWidth);
  
  useEffect(() => {
    const handleResize = () => setWindowWidth(window.innerWidth);
    
    // Add event listener
    window.addEventListener('resize', handleResize);
    
    // Cleanup: remove event listener
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);
  
  return <div>Window width: {windowWidth}px</div>;
}

Proper cleanup is essential for building robust React applications that don't leak memory or continue performing operations when they shouldn't.

Module Project

Apply your knowledge by building a React application that demonstrates side effects and data fetching.

Additional Resources