Explain the useEffect hook and its common use cases.

The useEffect hook is one of React’s built-in hooks that allows you to perform side effects in functional components. Side effects are operations that affect something outside the scope of the current function, such as data fetching, subscriptions, manual DOM manipulations, logging, and more.

Basic Syntax and Behavior

The useEffect hook takes two arguments: a function that contains the side effect code, and an optional dependency array.

useEffect(() => {
  // Side effect code here
  
  // Optional cleanup function
  return () => {
    // Cleanup code here
  };
}, [dependencies]); // Optional dependency array
  • The effect function runs after every render by default
  • The cleanup function (optional) runs before the component unmounts and before the effect runs again
  • The dependency array (optional) controls when the effect runs:
    • Omitted: Effect runs after every render
    • Empty array []: Effect runs only once after the initial render
    • With dependencies [a, b]: Effect runs when any dependency changes

Common Use Cases for useEffect

1. Data Fetching

One of the most common use cases for useEffect is fetching data from an API:

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(() => {
    // Reset states when userId changes
    setLoading(true);
    setError(null);
    
    // Fetch user data
    fetch(`https://api.example.com/users/${userId}`)
      .then(response => {
        if (!response.ok) throw new Error('Failed to fetch');
        return response.json();
      })
      .then(data => {
        setUser(data);
        setLoading(false);
      })
      .catch(err => {
        setError(err.message);
        setLoading(false);
      });
  }, [userId]); // Re-run when userId changes
  
  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;
  if (!user) return null;
  
  return (
    <div>
      <h1>{user.name}</h1>
      <p>Email: {user.email}</p>
    </div>
  );
}

2. Subscriptions and Event Listeners

useEffect is perfect for setting up and cleaning up subscriptions:

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

function WindowSizeTracker() {
  const [windowSize, setWindowSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });
  
  useEffect(() => {
    // Handler to call on window resize
    const handleResize = () => {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    };
    
    // Add event listener
    window.addEventListener('resize', handleResize);
    
    // Remove event listener on cleanup
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []); // Empty array means this effect runs once on mount
  
  return (
    <div>
      <p>Window width: {windowSize.width}px</p>
      <p>Window height: {windowSize.height}px</p>
    </div>
  );
}

3. DOM Manipulations

When you need to directly interact with the DOM:

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

function AutoFocusInput() {
  const inputRef = useRef(null);
  
  useEffect(() => {
    // Focus the input element after component mounts
    inputRef.current.focus();
  }, []);
  
  return (
    <input
      ref={inputRef}
      type="text"
      placeholder="This input will be focused automatically"
    />
  );
}

4. Updating Document Title

A simple but common use case is updating the document title:

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

function PageTitle({ title }) {
  useEffect(() => {
    // Update the document title
    document.title = title;
    
    // Optional: Restore the original title on unmount
    return () => {
      document.title = 'React App';
    };
  }, [title]); // Re-run when title prop changes
  
  return <h1>{title}</h1>;
}

5. Timer Management

Managing timers with proper cleanup:

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

function CountdownTimer({ seconds }) {
  const [timeLeft, setTimeLeft] = useState(seconds);
  
  useEffect(() => {
    // Don't start the timer if seconds is zero or negative
    if (seconds <= 0) return;
    
    // Reset timer when seconds prop changes
    setTimeLeft(seconds);
    
    // Set up the interval
    const intervalId = setInterval(() => {
      setTimeLeft(prevTime => {
        if (prevTime <= 1) {
          clearInterval(intervalId);
          return 0;
        }
        return prevTime - 1;
      });
    }, 1000);
    
    // Clean up the interval on unmount or when seconds changes
    return () => clearInterval(intervalId);
  }, [seconds]);
  
  return <div>Time remaining: {timeLeft} seconds</div>;
}

6. Syncing with External Systems

Integrating with third-party libraries:

import React, { useEffect, useRef } from 'react';
import Chart from 'chart.js/auto';

function SimpleChart({ data }) {
  const chartRef = useRef(null);
  const chartInstance = useRef(null);
  
  useEffect(() => {
    // If we already have a chart instance, destroy it
    if (chartInstance.current) {
      chartInstance.current.destroy();
    }
    
    // Create new chart
    const ctx = chartRef.current.getContext('2d');
    chartInstance.current = new Chart(ctx, {
      type: 'bar',
      data: data,
      options: {
        responsive: true,
        maintainAspectRatio: false
      }
    });
    
    // Cleanup function
    return () => {
      if (chartInstance.current) {
        chartInstance.current.destroy();
      }
    };
  }, [data]); // Re-run when data changes
  
  return (
    <div style={{ width: '300px', height: '200px' }}>
      <canvas ref={chartRef} />
    </div>
  );
}

Advanced Patterns with useEffect

Skipping Effects Conditionally

Sometimes you want to run an effect only under certain conditions:

useEffect(() => {
  if (isOnline) {
    // Only sync with server when online
    syncWithServer(data);
  }
}, [isOnline, data]);

Handling Race Conditions in Data Fetching

When fetching data that depends on props or state, you might encounter race conditions:

function SearchResults({ query }) {
  const [results, setResults] = useState([]);
  const [loading, setLoading] = useState(false);
  
  useEffect(() => {
    let isMounted = true;
    setLoading(true);
    
    fetch(`https://api.example.com/search?q=${query}`)
      .then(response => response.json())
      .then(data => {
        // Only update state if component is still mounted
        if (isMounted) {
          setResults(data);
          setLoading(false);
        }
      });
    
    // Cleanup function to handle unmounting
    return () => {
      isMounted = false;
    };
  }, [query]);
  
  // Component rendering
}

Debouncing with useEffect

For operations that shouldn’t happen too frequently (like search input):

function SearchInput() {
  const [searchTerm, setSearchTerm] = useState('');
  const [debouncedTerm, setDebouncedTerm] = useState(searchTerm);
  
  // Update searchTerm when input changes
  const handleChange = (e) => {
    setSearchTerm(e.target.value);
  };
  
  // Debounce the search term
  useEffect(() => {
    const timerId = setTimeout(() => {
      setDebouncedTerm(searchTerm);
    }, 500); // 500ms delay
    
    return () => {
      clearTimeout(timerId);
    };
  }, [searchTerm]);
  
  // Use debouncedTerm for API calls
  useEffect(() => {
    if (debouncedTerm) {
      // Make API call with debouncedTerm
      console.log('Searching for:', debouncedTerm);
    }
  }, [debouncedTerm]);
  
  return (
    <input
      type="text"
      value={searchTerm}
      onChange={handleChange}
      placeholder="Search..."
    />
  );
}

Real-World Example

Here’s a simple notification system that demonstrates several useEffect patterns:

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

function NotificationSystem() {
  const [notifications, setNotifications] = useState([]);
  const [isOnline, setIsOnline] = useState(navigator.onLine);
  
  // Effect for online/offline status
  useEffect(() => {
    const handleOnline = () => setIsOnline(true);
    const handleOffline = () => setIsOnline(false);
    
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);
  
  // Effect for fetching notifications
  useEffect(() => {
    if (!isOnline) return;
    
    const fetchNotifications = async () => {
      try {
        const response = await fetch('/api/notifications');
        const data = await response.json();
        setNotifications(data);
      } catch (error) {
        console.error('Failed to fetch notifications:', error);
      }
    };
    
    fetchNotifications();
    
    // Set up polling
    const intervalId = setInterval(fetchNotifications, 60000);
    
    return () => clearInterval(intervalId);
  }, [isOnline]);
  
  // Effect for notification sound
  useEffect(() => {
    if (notifications.length > 0 && notifications.some(n => !n.read)) {
      // Play notification sound
      const audio = new Audio('/notification-sound.mp3');
      audio.play().catch(e => console.log('Audio play failed:', e));
    }
  }, [notifications]);
  
  return (
    <div className="notification-panel">
      <h2>Notifications {!isOnline && '(offline)'}</h2>
      {notifications.length === 0 ? (
        <p>No new notifications</p>
      ) : (
        <ul>
          {notifications.map(notification => (
            <li key={notification.id} className={notification.read ? 'read' : 'unread'}>
              {notification.message}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

Common Mistakes and Pitfalls

1. Missing Dependencies

Forgetting to include all dependencies used inside the effect:

// Incorrect
useEffect(() => {
  console.log(`User ${userName} logged in`);
}, []); // Missing userName in dependencies

// Correct
useEffect(() => {
  console.log(`User ${userName} logged in`);
}, [userName]);

2. Infinite Loops

Creating an infinite loop by updating state in an effect without proper dependencies:

// This will cause an infinite loop!
useEffect(() => {
  setCount(count + 1);
}); // No dependency array

// Fixed version - only run once
useEffect(() => {
  setCount(c => c + 1);
}, []); // Empty dependency array

3. Forgetting Cleanup

Not cleaning up subscriptions, timers, or event listeners:

// Incorrect - memory leak
useEffect(() => {
  const subscription = someExternalAPI.subscribe();
  // No cleanup
}, []);

// Correct
useEffect(() => {
  const subscription = someExternalAPI.subscribe();
  return () => {
    subscription.unsubscribe();
  };
}, []);

Interview Tips

  1. Explain the purpose: useEffect is for side effects that can’t be done during rendering.

  2. Discuss the dependency array: Be ready to explain how the dependency array controls when effects run.

  3. Highlight cleanup: Emphasize the importance of the cleanup function for preventing memory leaks.

  4. Compare with lifecycle methods: Be prepared to compare useEffect with componentDidMount, componentDidUpdate, and componentWillUnmount.

  5. Mention common use cases: Data fetching, subscriptions, DOM manipulations, and timers are all good examples.

  6. Address performance concerns: Discuss how to optimize effects with proper dependencies and conditional execution.

  7. Talk about race conditions: Show your understanding of handling asynchronous operations properly.

  8. Discuss the rules: Remember that useEffect follows the Rules of Hooks - it can’t be called conditionally.

Test Your Knowledge

Take a quick quiz to test your understanding of this topic.