useEffect Hook for Side Effects

Mastering React's useEffect Hook for handling side effects in functional components

Introduction to Side Effects in React

In React components, side effects are operations that interact with the "outside world" or affect something beyond the component's scope. These include data fetching, subscriptions, manual DOM manipulations, logging, and more.

Real-world analogy: Think of side effects like errands you need to run while making dinner. Your main job is cooking (rendering UI), but you occasionally need to step away to check the mail, answer the door, or make a phone call (side effects). These activities aren't directly part of cooking, but they're necessary parts of your overall household management.

flowchart TD A[React Component] --> B[Rendering: Pure Function] A --> C[Side Effects: Impure Operations] B --> B1[Compute JSX] B --> B2[Return UI Elements] C --> C1[Data Fetching] C --> C2[Subscriptions] C --> C3[DOM Manipulation] C --> C4[Timers] C --> C5[Logging] style A fill:#f9f,stroke:#333,stroke-width:2px style C fill:#bbf,stroke:#33a,stroke-width:2px

Why We Need a Special Hook for Side Effects

React's rendering process should be pure and predictable - the same props and state should always produce the same output. Side effects break this purity because they interact with external systems.

The useEffect Hook provides a designated place to perform side effects, keeping them separate from the rendering logic and ensuring they run at the appropriate times.

The useEffect Hook: Basic Syntax

The useEffect Hook takes two arguments:

  1. A function that contains the side effect code
  2. An optional dependency array that controls when the effect runs

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

function ExampleComponent() {
  // State declaration
  const [count, setCount] = useState(0);
  
  // Effect declaration
  useEffect(() => {
    // Side effect code
    document.title = `You clicked ${count} times`;
    
    // Optional cleanup function
    return () => {
      document.title = 'React App';
    };
  }, [count]); // Dependency array
  
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}
      

When Does useEffect Run?

The timing of useEffect execution depends on the dependency array:

Dependency Array When the Effect Runs Use Case
useEffect(() => {...})
(no dependency array)
After every render Effects that need to run on every update
useEffect(() => {...}, [])
(empty array)
Only after the initial render Setup/initialization code, equivalent to componentDidMount
useEffect(() => {...}, [a, b])
(with dependencies)
After initial render and when any dependency changes Effects that depend on specific props or state
sequenceDiagram participant Component participant React participant Effect participant DOM Component->>React: Initial Render React->>DOM: Update DOM Note over DOM: Browser paints React->>Effect: Run Effect (first time) Note over Component: State Change Component->>React: Re-render React->>DOM: Update DOM Note over DOM: Browser paints React->>Effect: Check Dependencies Alt Dependencies Changed Effect->>Effect: Cleanup Previous Effect Effect->>Effect: Run Effect Again else No Change Note over Effect: Effect Skipped End

Effect Cleanup: Preventing Memory Leaks

Some effects create resources or subscriptions that need to be cleaned up before the component is unmounted or before the effect runs again. You can return a cleanup function from your effect to handle this.


function WindowSizeTracker() {
  const [windowSize, setWindowSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });
  
  useEffect(() => {
    // Setup effect: Add event listener
    const handleResize = () => {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    };
    
    window.addEventListener('resize', handleResize);
    
    // Cleanup function: Remove event listener
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []); // Empty array means run once on mount
  
  return (
    <div>
      <h3>Window Size:</h3>
      <p>Width: {windowSize.width}px</p>
      <p>Height: {windowSize.height}px</p>
    </div>
  );
}
      

When Does Cleanup Run?

The cleanup function runs:

Real-world analogy: If setting up an effect is like turning on a machine, the cleanup function is like turning it off when you're done. Just as you'd turn off the lights when leaving a room to save energy, you clean up effects to prevent memory leaks and unexpected behavior.

Common Resources That Need Cleanup

useEffect for Data Fetching

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

Basic Data Fetching Pattern


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);
    
    // Define async function inside effect
    async function fetchUser() {
      try {
        const response = await fetch(`https://api.example.com/users/${userId}`);
        
        if (!response.ok) {
          throw new Error(`HTTP error! Status: ${response.status}`);
        }
        
        const data = await response.json();
        setUser(data);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    }
    
    // Call the function
    fetchUser();
    
    // No cleanup needed for a simple fetch
  }, [userId]); // Re-run when userId changes
  
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  if (!user) return <div>No user found</div>;
  
  return (
    <div className="user-profile">
      <h2>{user.name}</h2>
      <p>Email: {user.email}</p>
      <p>Username: {user.username}</p>
      {/* Display more user data */}
    </div>
  );
}
      

Handling Race Conditions in Data Fetching

When fetching data based on changing props, you might encounter race conditions where an older fetch request completes after a newer one, causing stale data to override current data.


function SearchResults({ query }) {
  const [results, setResults] = useState([]);
  const [loading, setLoading] = useState(false);
  
  useEffect(() => {
    // Skip empty queries
    if (!query.trim()) {
      setResults([]);
      return;
    }
    
    let isMounted = true; // Flag to track if component is mounted
    setLoading(true);
    
    async function fetchResults() {
      try {
        const response = await fetch(
          `https://api.example.com/search?q=${encodeURIComponent(query)}`
        );
        const data = await response.json();
        
        // Only update state if component is still mounted
        // and this effect is still current
        if (isMounted) {
          setResults(data.results);
          setLoading(false);
        }
      } catch (error) {
        // Only update state if component is still mounted
        if (isMounted) {
          console.error('Search error:', error);
          setResults([]);
          setLoading(false);
        }
      }
    }
    
    fetchResults();
    
    // Cleanup function that runs when the component unmounts
    // or before the effect runs again
    return () => {
      isMounted = false;
    };
  }, [query]);
  
  return (
    <div>
      {loading ? (
        <p>Loading results for "{query}"...</p>
      ) : (
        <div>
          <h3>Results for "{query}"</h3>
          {results.length === 0 ? (
            <p>No results found</p>
          ) : (
            <ul>
              {results.map(item => (
                <li key={item.id}>{item.title}</li>
              ))}
            </ul>
          )}
        </div>
      )}
    </div>
  );
}
      

Note about React 18+: In React 18 and newer versions, state updates on unmounted components don't cause memory leaks or warnings anymore. However, it's still good practice to prevent unnecessary state updates, and the cleanup pattern is essential for proper cleanup of subscriptions, event listeners, etc.

Managing Timers with useEffect

useEffect is perfect for setting up and clearing timers in React components.

Simple Timer Example


function Timer() {
  const [seconds, setSeconds] = useState(0);
  const [isRunning, setIsRunning] = useState(false);
  
  useEffect(() => {
    // Only set up the timer if isRunning is true
    if (!isRunning) return;
    
    // Set up the interval
    const intervalId = setInterval(() => {
      setSeconds(prevSeconds => prevSeconds + 1);
    }, 1000);
    
    // Clean up the interval when the effect is re-run or the component unmounts
    return () => {
      clearInterval(intervalId);
    };
  }, [isRunning]); // Re-run when isRunning changes
  
  const handleStartStop = () => {
    setIsRunning(prevIsRunning => !prevIsRunning);
  };
  
  const handleReset = () => {
    setSeconds(0);
    setIsRunning(false);
  };
  
  // Format seconds as mm:ss
  const formatTime = () => {
    const minutes = Math.floor(seconds / 60);
    const remainingSeconds = seconds % 60;
    return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
  };
  
  return (
    <div className="timer">
      <div className="timer-display">{formatTime()}</div>
      
      <div className="timer-controls">
        <button onClick={handleStartStop}>
          {isRunning ? 'Pause' : 'Start'}
        </button>
        
        <button onClick={handleReset}>
          Reset
        </button>
      </div>
    </div>
  );
}
      

Debouncing with useEffect

Debouncing is a technique to limit how often a function is called. It's useful for search inputs, window resizing, and other events that can fire rapidly.


function DebouncedSearch() {
  const [searchTerm, setSearchTerm] = useState('');
  const [debouncedTerm, setDebouncedTerm] = useState('');
  const [results, setResults] = useState([]);
  const [isSearching, setIsSearching] = useState(false);
  
  // Update searchTerm as the user types
  const handleChange = (e) => {
    setSearchTerm(e.target.value);
  };
  
  // Debounce the search term
  useEffect(() => {
    // Set a timeout to update the debounced term
    const timeoutId = setTimeout(() => {
      setDebouncedTerm(searchTerm);
    }, 500); // 500ms delay
    
    // Clean up the timeout if searchTerm changes before timeout completes
    return () => {
      clearTimeout(timeoutId);
    };
  }, [searchTerm]);
  
  // Perform the search when debouncedTerm changes
  useEffect(() => {
    // Skip empty searches
    if (!debouncedTerm.trim()) {
      setResults([]);
      return;
    }
    
    // Set loading state
    setIsSearching(true);
    
    // Define async search function
    async function fetchResults() {
      try {
        const response = await fetch(
          `https://api.example.com/search?q=${encodeURIComponent(debouncedTerm)}`
        );
        const data = await response.json();
        setResults(data.results);
      } catch (error) {
        console.error('Search failed:', error);
        setResults([]);
      } finally {
        setIsSearching(false);
      }
    }
    
    // Execute the search
    fetchResults();
  }, [debouncedTerm]);
  
  return (
    <div>
      <input
        type="text"
        value={searchTerm}
        onChange={handleChange}
        placeholder="Type to search..."
      />
      
      {isSearching && <p>Searching...</p>}
      
      <ul>
        {results.map(item => (
          <li key={item.id}>{item.title}</li>
        ))}
      </ul>
      
      {!isSearching && debouncedTerm && results.length === 0 && (
        <p>No results found for "{debouncedTerm}"</p>
      )}
    </div>
  );
}
      

useEffect for DOM Manipulation

While React generally handles DOM updates, sometimes you need direct DOM manipulation for specific use cases like focusing elements, measuring dimensions, or integrating with third-party libraries.

Auto-Focus Input Example


function AutoFocusInput() {
  const inputRef = useRef(null);
  
  // Focus the input when the component mounts
  useEffect(() => {
    if (inputRef.current) {
      inputRef.current.focus();
    }
  }, []); // Empty dependency array means this runs once on mount
  
  return (
    <div>
      <label htmlFor="auto-focus-input">This input will focus automatically:</label>
      <input
        id="auto-focus-input"
        ref={inputRef}
        type="text"
      />
    </div>
  );
}
      

Measuring DOM Elements


function ElementSize() {
  const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
  const elementRef = useRef(null);
  
  // Measure the element after it's rendered
  useEffect(() => {
    if (!elementRef.current) return;
    
    // Create a ResizeObserver to track size changes
    const resizeObserver = new ResizeObserver(entries => {
      const { width, height } = entries[0].contentRect;
      setDimensions({ width, height });
    });
    
    // Start observing our element
    resizeObserver.observe(elementRef.current);
    
    // Clean up observer on unmount
    return () => {
      resizeObserver.disconnect();
    };
  }, []); // Empty dependency array - run once on mount
  
  return (
    <div>
      <div
        ref={elementRef}
        style={{
          width: '50%',
          minHeight: '100px',
          padding: '20px',
          background: '#f0f0f0',
          resize: 'both',
          overflow: 'auto',
          border: '1px solid #ccc'
        }}
      >
        <p>This element is resizable. Try resizing it!</p>
      </div>
      
      <p>Element dimensions: {dimensions.width.toFixed(0)}px × {dimensions.height.toFixed(0)}px</p>
    </div>
  );
}
      

Integrating with Third-Party Libraries


function ChartComponent({ data }) {
  const chartRef = useRef(null);
  const chartInstance = useRef(null);
  
  // Initialize the chart when the component mounts
  // or when data changes
  useEffect(() => {
    if (!chartRef.current) return;
    
    // Clean up any existing chart
    if (chartInstance.current) {
      chartInstance.current.destroy();
    }
    
    // Create a new chart using a fictional Chart library
    const ctx = chartRef.current.getContext('2d');
    chartInstance.current = new Chart(ctx, {
      type: 'bar',
      data: {
        labels: data.map(item => item.label),
        datasets: [{
          label: 'Value',
          data: data.map(item => item.value),
          backgroundColor: 'rgba(75, 192, 192, 0.6)'
        }]
      },
      options: {
        responsive: true,
        maintainAspectRatio: false
      }
    });
    
    // Clean up the chart when the component unmounts
    // or before the effect runs again
    return () => {
      if (chartInstance.current) {
        chartInstance.current.destroy();
        chartInstance.current = null;
      }
    };
  }, [data]); // Re-run when data changes
  
  return (
    <div style={{ height: '300px' }}>
      <canvas ref={chartRef} />
    </div>
  );
}
      

Optimizing useEffect

Common Dependency Array Mistakes

❌ Missing Dependencies


function Counter({ initial }) {
  const [count, setCount] = useState(initial);
  
  // Missing dependency: initial
  useEffect(() => {
    setCount(initial);
  }, []); // Should include 'initial'
}
          

This effect won't run when the 'initial' prop changes.

✅ Correct Dependencies


function Counter({ initial }) {
  const [count, setCount] = useState(initial);
  
  useEffect(() => {
    setCount(initial);
  }, [initial]); // Includes all dependencies
}
          

❌ Excessive Dependencies


function ProfileCard({ user }) {
  // The entire user object as a dependency
  useEffect(() => {
    document.title = `Profile: ${user.name}`;
  }, [user]); // Re-runs on ANY change to user object
}
          

This will re-run if any property of user changes, not just the name.

✅ Precise Dependencies


function ProfileCard({ user }) {
  // Only the specific property we need
  useEffect(() => {
    document.title = `Profile: ${user.name}`;
  }, [user.name]); // Only re-runs when name changes
}
          

Avoiding Infinite Loops

❌ Infinite Loop


function InfiniteCounter() {
  const [count, setCount] = useState(0);
  
  // This creates an infinite loop!
  useEffect(() => {
    setCount(count + 1);
  }, [count]); // Effect changes its own dependency
  
  return <div>Count: {count}</div>;
}
          

✅ Controlled Update


function ControlledCounter() {
  const [count, setCount] = useState(0);
  
  // Only run once on mount
  useEffect(() => {
    setCount(1); // Set initial value once
  }, []); // Empty dependency array
  
  // Or use a condition
  useEffect(() => {
    if (count < 5) { // Limit the updates
      setCount(count + 1);
    }
  }, [count]);
  
  return <div>Count: {count}</div>;
}
          

Using Functional Updates

When updating state based on its previous value, use the functional update form to avoid unnecessary dependencies:

⚠️ Suboptimal


function Counter() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    const timer = setInterval(() => {
      setCount(count + 1); // Depends on current count
    }, 1000);
    
    return () => clearInterval(timer);
  }, [count]); // Need count as dependency
  
  return <div>Count: {count}</div>;
}
          

This will create a new interval every time count changes.

✅ Optimal


function Counter() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    const timer = setInterval(() => {
      setCount(prevCount => prevCount + 1); // Functional update
    }, 1000);
    
    return () => clearInterval(timer);
  }, []); // No dependencies needed
  
  return <div>Count: {count}</div>;
}
          

This creates the interval once and updates the count correctly.

Using Refs to Avoid Dependencies

Use refs to track values that you need in an effect but don't want to trigger re-runs:


function LoggerComponent({ value }) {
  // Store the latest value in a ref
  const valueRef = useRef(value);
  
  // Update the ref when value changes
  useEffect(() => {
    valueRef.current = value;
  }, [value]);
  
  // Use an interval that doesn't need to re-run when value changes
  useEffect(() => {
    const intervalId = setInterval(() => {
      // Access the latest value from the ref
      console.log('Current value:', valueRef.current);
    }, 2000);
    
    return () => clearInterval(intervalId);
  }, []); // Empty dependency array
  
  return <div>Value: {value}</div>;
}
      

Advanced useEffect Patterns

Component Lifecycle with useEffect

You can replicate class component lifecycle methods using useEffect:


function LifecycleComponent() {
  // componentDidMount
  useEffect(() => {
    console.log('Component mounted');
    
    // componentWillUnmount
    return () => {
      console.log('Component will unmount');
    };
  }, []);
  
  // componentDidUpdate for specific prop/state
  const [count, setCount] = useState(0);
  useEffect(() => {
    if (count > 0) { // Skip first render
      console.log('Count updated to:', count);
    }
  }, [count]);
  
  // Run on every render (componentDidUpdate without checks)
  useEffect(() => {
    console.log('Component rendered');
  });
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}
      

Separating Concerns with Multiple Effects

It's better to split unrelated effects into separate useEffect calls:


function UserDashboard({ userId }) {
  const [user, setUser] = useState(null);
  const [posts, setPosts] = useState([]);
  const [isOnline, setIsOnline] = useState(navigator.onLine);
  
  // Effect for user data
  useEffect(() => {
    async function fetchUser() {
      const userData = await api.getUser(userId);
      setUser(userData);
    }
    
    fetchUser();
  }, [userId]);
  
  // Separate effect for posts
  useEffect(() => {
    if (!user) return; // Dependent on user being loaded
    
    async function fetchPosts() {
      const userPosts = await api.getUserPosts(user.id);
      setPosts(userPosts);
    }
    
    fetchPosts();
  }, [user]);
  
  // Separate effect for online status
  useEffect(() => {
    function handleOnline() {
      setIsOnline(true);
    }
    
    function handleOffline() {
      setIsOnline(false);
    }
    
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);
  
  if (!user) return <div>Loading...</div>;
  
  return (
    <div>
      <div className="user-info">
        <h2>{user.name}</h2>
        <span className={isOnline ? 'status-online' : 'status-offline'}>
          {isOnline ? 'Online' : 'Offline'}
        </span>
      </div>
      
      <h3>Recent Posts</h3>
      <ul>
        {posts.map(post => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  );
}
      

Creating Custom Hooks with useEffect

Extract reusable effect logic into custom hooks:


// Custom hook for window size
function useWindowSize() {
  const [windowSize, setWindowSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });
  
  useEffect(() => {
    function handleResize() {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    }
    
    window.addEventListener('resize', handleResize);
    
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []); // Empty dependency - only run on mount/unmount
  
  return windowSize;
}

// Custom hook for API data
function useApiData(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    let isMounted = true;
    
    async function fetchData() {
      try {
        setLoading(true);
        const response = await fetch(url);
        
        if (!response.ok) {
          throw new Error(`HTTP error! Status: ${response.status}`);
        }
        
        const result = await response.json();
        
        if (isMounted) {
          setData(result);
          setError(null);
        }
      } catch (err) {
        if (isMounted) {
          setError(err.message);
          setData(null);
        }
      } finally {
        if (isMounted) {
          setLoading(false);
        }
      }
    }
    
    fetchData();
    
    return () => {
      isMounted = false;
    };
  }, [url]);
  
  return { data, loading, error };
}

// Usage example
function DataDisplay() {
  const windowSize = useWindowSize();
  const { data, loading, error } = useApiData('https://api.example.com/data');
  
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  
  return (
    <div>
      <p>Window size: {windowSize.width} x {windowSize.height}</p>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
}
      

Real-World Applications

Form Auto-Save


function AutoSaveForm() {
  const [formData, setFormData] = useState({
    title: '',
    content: ''
  });
  const [lastSaved, setLastSaved] = useState(null);
  const [isSaving, setIsSaving] = useState(false);
  const [saveStatus, setSaveStatus] = useState('idle');
  
  // Load initial data
  useEffect(() => {
    // Try to load from localStorage first
    const savedData = localStorage.getItem('draft');
    
    if (savedData) {
      try {
        setFormData(JSON.parse(savedData));
        setLastSaved(new Date());
        setSaveStatus('loaded');
      } catch (e) {
        console.error('Failed to parse saved data:', e);
      }
    }
  }, []); // Empty dependency array - run once on mount
  
  // Auto-save effect
  useEffect(() => {
    // Skip the initial render or empty form
    if (saveStatus === 'idle' || (!formData.title && !formData.content)) {
      return;
    }
    
    // Set up a timer for auto-save
    const timerId = setTimeout(() => {
      setIsSaving(true);
      setSaveStatus('saving');
      
      // Simulate saving to server
      setTimeout(() => {
        // Save to localStorage for demo
        localStorage.setItem('draft', JSON.stringify(formData));
        setLastSaved(new Date());
        setIsSaving(false);
        setSaveStatus('saved');
      }, 500);
    }, 1000); // Auto-save 1 second after typing stops
    
    // Clear timeout if formData changes again before save triggers
    return () => {
      clearTimeout(timerId);
    };
  }, [formData, saveStatus]);
  
  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData(prev => ({
      ...prev,
      [name]: value
    }));
    
    if (saveStatus === 'saved') {
      setSaveStatus('editing');
    }
  };
  
  const handleSubmit = (e) => {
    e.preventDefault();
    
    // Here you would normally send the data to a server
    alert('Form submitted: ' + JSON.stringify(formData));
    
    // Clear the draft
    localStorage.removeItem('draft');
    setFormData({ title: '', content: '' });
    setLastSaved(null);
    setSaveStatus('idle');
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="title">Title:</label>
        <input
          id="title"
          name="title"
          value={formData.title}
          onChange={handleChange}
          required
        />
      </div>
      
      <div>
        <label htmlFor="content">Content:</label>
        <textarea
          id="content"
          name="content"
          value={formData.content}
          onChange={handleChange}
          rows="6"
          required
        />
      </div>
      
      <div className="form-status">
        {isSaving && <span>Saving...</span>}
        {!isSaving && lastSaved && (
          <span>Last saved: {lastSaved.toLocaleTimeString()}</span>
        )}
      </div>
      
      <button type="submit">Submit</button>
    </form>
  );
}
      

Real-Time Updates with WebSocket


function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);
  const [connected, setConnected] = useState(false);
  const [inputMessage, setInputMessage] = useState('');
  const socketRef = useRef(null);
  
  // Effect for connecting to WebSocket
  useEffect(() => {
    // Create a new WebSocket connection
    const socket = new WebSocket(`wss://chat.example.com/rooms/${roomId}`);
    socketRef.current = socket;
    
    // Set up event handlers
    socket.addEventListener('open', () => {
      console.log('Connected to chat room:', roomId);
      setConnected(true);
    });
    
    socket.addEventListener('message', (event) => {
      const message = JSON.parse(event.data);
      setMessages(prev => [...prev, message]);
    });
    
    socket.addEventListener('close', () => {
      console.log('Disconnected from chat room');
      setConnected(false);
    });
    
    socket.addEventListener('error', (error) => {
      console.error('WebSocket error:', error);
      setConnected(false);
    });
    
    // Clean up when component unmounts or roomId changes
    return () => {
      console.log('Closing WebSocket connection');
      socket.close();
    };
  }, [roomId]); // Re-connect if room changes
  
  // Function to send a message
  const sendMessage = (e) => {
    e.preventDefault();
    
    if (!inputMessage.trim() || !connected) return;
    
    const message = {
      type: 'chat',
      content: inputMessage,
      sender: 'Me', // In a real app, this would be the user's identity
      timestamp: new Date().toISOString()
    };
    
    socketRef.current.send(JSON.stringify(message));
    setInputMessage('');
  };
  
  return (
    <div className="chat-room">
      <div className="chat-header">
        <h2>Chat Room: {roomId}</h2>
        <div className={`connection-status ${connected ? 'connected' : 'disconnected'}`}>
          {connected ? 'Connected' : 'Disconnected'}
        </div>
      </div>
      
      <div className="chat-messages">
        {messages.length === 0 ? (
          <p className="no-messages">No messages yet.</p>
        ) : (
          messages.map((msg, index) => (
            <div 
              key={index} 
              className={`message ${msg.sender === 'Me' ? 'sent' : 'received'}`}
            >
              <div className="message-sender">{msg.sender}</div>
              <div className="message-content">{msg.content}</div>
              <div className="message-time">
                {new Date(msg.timestamp).toLocaleTimeString()}
              </div>
            </div>
          ))
        )}
      </div>
      
      <form className="chat-input" onSubmit={sendMessage}>
        <input
          type="text"
          value={inputMessage}
          onChange={(e) => setInputMessage(e.target.value)}
          placeholder="Type a message..."
          disabled={!connected}
        />
        <button type="submit" disabled={!connected}>Send</button>
      </form>
    </div>
  );
}
      

Practice Exercises

Exercise 1: Data Fetching Component

Create a component that fetches and displays data from a public API, with the following features:

Use the JSONPlaceholder API: https://jsonplaceholder.typicode.com/posts

Exercise 2: Countdown Timer

Build a countdown timer that:

Exercise 3: Form with Auto-Complete

Create a form with an auto-complete feature:

You can use the GitHub Users API for suggestions: https://api.github.com/search/users?q={query}

Summary

In this lecture, we've covered:

The useEffect Hook is a powerful tool that lets you synchronize your React components with external systems. By mastering useEffect, you'll be able to create components that interact smoothly with APIs, timers, DOM elements, and other external resources while maintaining React's declarative programming model.

Further Resources