State and Side Effects Integration

1. useEffect Hook with State Dependencies

useEffect Hook Syntax and State Dependencies

Pattern Syntax Behavior
Basic useEffect useEffect(() => { /* effect */ }) Runs after every render
Empty Dependencies useEffect(() => { /* effect */ }, []) Runs only on mount
State Dependencies useEffect(() => { /* effect */ }, [state]) Runs when state changes
Multiple Dependencies useEffect(() => { /* effect */ }, [a, b, c]) Runs when any dependency changes
With Cleanup useEffect(() => { return () => cleanup(); }, [deps]) Cleanup runs before next effect and unmount

Common useEffect with State Patterns

Pattern Use Case Example
Fetch Data on State Change Load data when filter/query changes useEffect(() => fetchData(query), [query])
Sync State to localStorage Persist state changes useEffect(() => localStorage.set(key, state), [state])
Update Document Title Reflect state in browser title useEffect(() => document.title = title, [title])
Focus Input on State Focus element when condition changes useEffect(() => inputRef.current?.focus(), [isEdit])
Trigger Animation Start animation on state change useEffect(() => animate(), [isVisible])

Example: Fetch Data on State Change

const UserList = () => {
  const [users, setUsers] = useState([]);
  const [searchTerm, setSearchTerm] = useState('');
  const [loading, setLoading] = useState(false);

  // ✅ Fetch data when searchTerm changes
  useEffect(() => {
    const fetchUsers = async () => {
      setLoading(true);
      try {
        const response = await fetch(`/api/users?search=${searchTerm}`);
        const data = await response.json();
        setUsers(data);
      } catch (error) {
        console.error('Failed to fetch users:', error);
      } finally {
        setLoading(false);
      }
    };

    if (searchTerm) {
      fetchUsers();
    } else {
      setUsers([]); // Clear users when search is empty
    }
  }, [searchTerm]); // Re-run when searchTerm changes

  return (
    <div>
      <input 
        value={searchTerm} 
        onChange={(e) => setSearchTerm(e.target.value)}
        placeholder="Search users..."
      />
      {loading ? <p>Loading...</p> : <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>}
    </div>
  );
};

Example: Sync State to localStorage

const usePersistentState = (key, initialValue) => {
  // Initialize from localStorage
  const [state, setState] = useState(() => {
    const stored = localStorage.getItem(key);
    return stored ? JSON.parse(stored) : initialValue;
  });

  // ✅ Sync to localStorage whenever state changes
  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(state));
  }, [key, state]); // Dependencies include key and state

  return [state, setState];
};

// Usage
const TodoApp = () => {
  const [todos, setTodos] = usePersistentState('todos', []);
  
  const addTodo = (text) => {
    setTodos(prev => [...prev, { id: Date.now(), text }]);
  };
  
  // State automatically persists to localStorage
  return <div>{/* ... */}</div>;
};
Warning: Avoid infinite loops! Don't update a state variable inside useEffect that's also in the dependency array without proper conditions. Always include all dependencies that are used inside the effect to avoid stale closures.

Example: ❌ Infinite Loop Anti-pattern

// ❌ BAD - Infinite loop!
const BadComponent = () => {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    setCount(count + 1); // Updates count on every render
  }, [count]); // count is dependency, creates infinite loop!
  
  return <div>{count}</div>;
};

// ✅ GOOD - Conditional update
const GoodComponent = () => {
  const [count, setCount] = useState(0);
  const [maxReached, setMaxReached] = useState(false);
  
  useEffect(() => {
    if (count < 10 && !maxReached) {
      setCount(count + 1);
    } else {
      setMaxReached(true);
    }
  }, [count, maxReached]); // Safe with condition
  
  return <div>{count}</div>;
};

2. State Synchronization with External Systems

External System Synchronization Patterns

System Type Synchronization Method Use Case
Browser APIs useEffect with state dependencies Window resize, scroll, media queries
Web Storage useEffect + storage events localStorage, sessionStorage persistence
WebSocket useEffect with connection management Real-time data updates
Third-party Libraries useSyncExternalStore (React 18+) Redux, MobX, external stores
Timers/Intervals useEffect with cleanup Polling, countdowns, animations
DOM Mutations useEffect with MutationObserver Track DOM changes outside React

useSyncExternalStore Hook (React 18+)

Feature Description Example
Purpose Subscribe to external store safely Concurrent rendering compatible
Syntax useSyncExternalStore(subscribe, getSnapshot) Returns current snapshot value
Subscribe Function (callback) => unsubscribe Called when store subscribes/unsubscribes
GetSnapshot Function () => value Returns current store value
Tearing Prevention Prevents inconsistent UI during concurrent render Critical for external stores

Example: Window Resize State Synchronization

const useWindowSize = () => {
  const [windowSize, setWindowSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });

  useEffect(() => {
    // ✅ Sync state with window resize events
    const handleResize = () => {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    };

    window.addEventListener('resize', handleResize);
    
    // Cleanup subscription on unmount
    return () => window.removeEventListener('resize', handleResize);
  }, []); // Empty deps - setup once on mount

  return windowSize;
};

// Usage
const ResponsiveComponent = () => {
  const { width, height } = useWindowSize();
  
  return (
    <div>
      <p>Window size: {width} x {height}</p>
      {width < 768 ? <MobileView /> : <DesktopView />}
    </div>
  );
};

Example: WebSocket State Synchronization

const useWebSocket = (url) => {
  const [messages, setMessages] = useState([]);
  const [isConnected, setIsConnected] = useState(false);

  useEffect(() => {
    // ✅ Connect to WebSocket and sync messages to state
    const ws = new WebSocket(url);

    ws.onopen = () => {
      setIsConnected(true);
      console.log('WebSocket connected');
    };

    ws.onmessage = (event) => {
      const message = JSON.parse(event.data);
      setMessages(prev => [...prev, message]);
    };

    ws.onerror = (error) => {
      console.error('WebSocket error:', error);
      setIsConnected(false);
    };

    ws.onclose = () => {
      setIsConnected(false);
      console.log('WebSocket disconnected');
    };

    // ✅ Cleanup - close connection on unmount
    return () => {
      ws.close();
    };
  }, [url]); // Reconnect if URL changes

  const sendMessage = (message) => {
    if (ws && ws.readyState === WebSocket.OPEN) {
      ws.send(JSON.stringify(message));
    }
  };

  return { messages, isConnected, sendMessage };
};

Example: useSyncExternalStore with Custom Store

// Custom external store
class CounterStore {
  constructor() {
    this.count = 0;
    this.listeners = new Set();
  }

  subscribe = (callback) => {
    this.listeners.add(callback);
    return () => this.listeners.delete(callback);
  };

  getSnapshot = () => {
    return this.count;
  };

  increment = () => {
    this.count++;
    this.listeners.forEach(listener => listener());
  };
}

const counterStore = new CounterStore();

// ✅ Use with useSyncExternalStore (React 18+)
const useCounter = () => {
  const count = useSyncExternalStore(
    counterStore.subscribe,
    counterStore.getSnapshot
  );

  return { count, increment: counterStore.increment };
};

// Usage
const Counter = () => {
  const { count, increment } = useCounter();
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
};
Note: When syncing with external systems, always clean up subscriptions in the useEffect cleanup function to prevent memory leaks. Use useSyncExternalStore for external stores in React 18+ to ensure concurrent rendering compatibility.

3. Cleanup Functions and State Management

Cleanup Function Patterns

Pattern Purpose Example
Event Listener Cleanup Remove DOM event listeners return () => el.removeEventListener()
Timer Cleanup Clear timeouts and intervals return () => clearTimeout(timerId)
Subscription Cleanup Unsubscribe from observables return () => subscription.unsubscribe()
Request Cancellation Cancel pending async requests return () => abortController.abort()
Connection Cleanup Close WebSocket/network connections return () => ws.close()
Resource Release Release memory, object URLs return () => URL.revokeObjectURL(url)

Cleanup Function Execution Timing

Scenario When Cleanup Runs Purpose
Component Unmount Before component is removed from DOM Final cleanup before removal
Effect Re-run Before effect runs again Clean up previous effect's resources
Dependencies Change Before effect with new dependencies Reset for new dependency values
Strict Mode (Dev) After mount, then remount immediately Test cleanup function correctness

Example: Timer Cleanup

const Countdown = ({ initialSeconds }) => {
  const [seconds, setSeconds] = useState(initialSeconds);
  const [isActive, setIsActive] = useState(false);

  useEffect(() => {
    if (!isActive) return; // Don't start timer if not active

    // ✅ Start interval
    const intervalId = setInterval(() => {
      setSeconds(prev => {
        if (prev <= 1) {
          setIsActive(false);
          return 0;
        }
        return prev - 1;
      });
    }, 1000);

    // ✅ Cleanup - clear interval on unmount or when isActive changes
    return () => {
      clearInterval(intervalId);
      console.log('Timer cleaned up');
    };
  }, [isActive]); // Re-run when isActive changes

  return (
    <div>
      <p>Time remaining: {seconds}s</p>
      <button onClick={() => setIsActive(!isActive)}>
        {isActive ? 'Pause' : 'Start'}
      </button>
    </div>
  );
};

Example: Abort Controller for Request Cancellation

const UserProfile = ({ userId }) => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    // ✅ Create AbortController for this request
    const abortController = new AbortController();
    
    const fetchUser = async () => {
      setLoading(true);
      setError(null);
      
      try {
        const response = await fetch(`/api/users/${userId}`, {
          signal: abortController.signal // Pass abort signal
        });
        
        if (!response.ok) throw new Error('Failed to fetch');
        
        const data = await response.json();
        setUser(data);
      } catch (err) {
        // ✅ Ignore abort errors (expected on cleanup)
        if (err.name === 'AbortError') {
          console.log('Request aborted');
          return;
        }
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    fetchUser();

    // ✅ Cleanup - abort pending request
    return () => {
      abortController.abort();
      console.log('Fetch aborted for userId:', userId);
    };
  }, [userId]); // Re-run when userId changes, canceling previous request

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;
  return <div>{user?.name}</div>;
};

Example: Multiple Cleanup Operations

const ChatRoom = ({ roomId }) => {
  const [messages, setMessages] = useState([]);
  const [isConnected, setIsConnected] = useState(false);

  useEffect(() => {
    let ws;
    let reconnectTimer;

    const connect = () => {
      ws = new WebSocket(`wss://chat.example.com/${roomId}`);

      ws.onopen = () => setIsConnected(true);
      ws.onmessage = (event) => {
        const message = JSON.parse(event.data);
        setMessages(prev => [...prev, message]);
      };
      ws.onclose = () => {
        setIsConnected(false);
        // Attempt reconnect after 5 seconds
        reconnectTimer = setTimeout(connect, 5000);
      };
    };

    connect();

    // ✅ Cleanup multiple resources
    return () => {
      console.log('Cleaning up chat room:', roomId);
      
      // Clear reconnect timer if pending
      if (reconnectTimer) {
        clearTimeout(reconnectTimer);
      }
      
      // Close WebSocket connection
      if (ws) {
        ws.onclose = null; // Prevent reconnect attempt
        ws.close();
      }
      
      // Clear messages on room change
      setMessages([]);
    };
  }, [roomId]); // Cleanup and reconnect when roomId changes

  return (
    <div>
      <p>Status: {isConnected ? 'Connected' : 'Disconnected'}</p>
      <ul>{messages.map((m, i) => <li key={i}>{m.text}</li>)}</ul>
    </div>
  );
};
Warning: Always return a cleanup function from useEffect when dealing with subscriptions, timers, or async operations. Forgetting cleanup can lead to memory leaks, state updates on unmounted components, and race conditions.

4. State and Subscription Patterns

Subscription Pattern Types

Pattern Implementation Use Case
Observable Subscription useEffect + observable.subscribe() RxJS, streams, event emitters
Event Emitter Pattern useEffect + emitter.on/off Node.js EventEmitter, custom events
PubSub Pattern useEffect + subscribe/unsubscribe Message buses, global events
Store Subscription useSyncExternalStore Redux, Zustand, external stores
Real-time Data useEffect + WebSocket/SSE Live updates, notifications

Subscription Best Practices

Practice Recommendation Benefit
Always Unsubscribe Return cleanup function Prevent memory leaks
Stable Callbacks Use useCallback for handlers Avoid unnecessary resubscriptions
Error Handling Handle subscription errors gracefully Robust error recovery
Loading States Track subscription connection status Better UX with loading indicators
Reconnection Logic Implement exponential backoff Resilient connections

Example: RxJS Observable Subscription

import { interval } from 'rxjs';
import { map, filter } from 'rxjs/operators';

const useObservable = (observable$, initialValue) => {
  const [value, setValue] = useState(initialValue);
  const [error, setError] = useState(null);

  useEffect(() => {
    // ✅ Subscribe to observable
    const subscription = observable$.subscribe({
      next: (data) => setValue(data),
      error: (err) => setError(err),
      complete: () => console.log('Observable completed')
    });

    // ✅ Cleanup - unsubscribe
    return () => {
      subscription.unsubscribe();
      console.log('Unsubscribed from observable');
    };
  }, [observable$]);

  return { value, error };
};

// Usage
const TickerComponent = () => {
  // Create observable that emits even numbers every second
  const ticker$ = interval(1000).pipe(
    map(n => n * 2),
    filter(n => n < 20)
  );

  const { value, error } = useObservable(ticker$, 0);

  if (error) return <p>Error: {error.message}</p>;
  return <p>Current value: {value}</p>;
};

Example: Event Emitter Subscription

// Custom event emitter
class EventBus {
  constructor() {
    this.listeners = new Map();
  }

  on(event, callback) {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, new Set());
    }
    this.listeners.get(event).add(callback);
  }

  off(event, callback) {
    if (this.listeners.has(event)) {
      this.listeners.get(event).delete(callback);
    }
  }

  emit(event, data) {
    if (this.listeners.has(event)) {
      this.listeners.get(event).forEach(callback => callback(data));
    }
  }
}

const eventBus = new EventBus();

// ✅ Custom hook for event subscription
const useEventBus = (event, handler) => {
  useEffect(() => {
    // Subscribe to event
    eventBus.on(event, handler);

    // ✅ Cleanup - unsubscribe
    return () => {
      eventBus.off(event, handler);
      console.log(`Unsubscribed from ${event}`);
    };
  }, [event, handler]); // Re-subscribe if event or handler changes
};

// Usage
const NotificationListener = () => {
  const [notifications, setNotifications] = useState([]);

  // ✅ Subscribe to 'notification' events
  useEventBus('notification', (data) => {
    setNotifications(prev => [...prev, data]);
  });

  return (
    <ul>
      {notifications.map((n, i) => <li key={i}>{n.message}</li>)}
    </ul>
  );
};

Example: Server-Sent Events (SSE) Subscription

const useServerSentEvents = (url) => {
  const [events, setEvents] = useState([]);
  const [isConnected, setIsConnected] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    // ✅ Create EventSource for SSE
    const eventSource = new EventSource(url);

    eventSource.onopen = () => {
      setIsConnected(true);
      setError(null);
      console.log('SSE connection opened');
    };

    eventSource.onmessage = (event) => {
      const data = JSON.parse(event.data);
      setEvents(prev => [...prev, data]);
    };

    eventSource.onerror = (err) => {
      setIsConnected(false);
      setError('Connection failed');
      console.error('SSE error:', err);
    };

    // ✅ Cleanup - close connection
    return () => {
      eventSource.close();
      console.log('SSE connection closed');
    };
  }, [url]); // Reconnect if URL changes

  return { events, isConnected, error };
};

// Usage
const LiveFeed = () => {
  const { events, isConnected, error } = useServerSentEvents('/api/live-feed');

  return (
    <div>
      <p>Status: {isConnected ? 'Connected' : 'Disconnected'}</p>
      {error && <p>Error: {error}</p>}
      <ul>{events.map((e, i) => <li key={i}>{e.message}</li>)}</ul>
    </div>
  );
};
Best Practice: When working with subscriptions, use useCallback to memoize event handlers to prevent unnecessary unsubscribe/resubscribe cycles. Always test that cleanup functions properly unsubscribe to avoid memory leaks.

5. Race Condition Prevention in State Updates

Common Race Condition Scenarios

Scenario Problem Solution
Multiple Async Requests Older request completes after newer one AbortController, request ID tracking
Rapid State Changes Intermediate states get overwritten Debouncing, throttling, latest request wins
Stale Closure Effect uses old state value Functional state updates, proper dependencies
Unmounted Component Update setState called after unmount Cleanup flag, AbortSignal
Concurrent Requests Multiple requests update same state Request deduplication, cancel in-flight

Race Condition Prevention Techniques

Technique Implementation Use Case
AbortController Cancel previous requests Search, autocomplete, pagination
Request ID Tracking Ignore responses from stale requests Sequential requests with changing params
Cleanup Flag Track if component is mounted Prevent state updates after unmount
Debouncing Delay request until input settles Search input, form validation
Optimistic Updates Update UI immediately, rollback on error Form submissions, toggle actions
State Machines Explicit state transitions Complex async flows

Example: AbortController to Prevent Race Conditions

const SearchResults = () => {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    if (!query) {
      setResults([]);
      return;
    }

    // ✅ Create AbortController for this search
    const abortController = new AbortController();
    
    const searchProducts = async () => {
      setLoading(true);
      
      try {
        const response = await fetch(`/api/search?q=${query}`, {
          signal: abortController.signal // Cancel if query changes
        });
        
        const data = await response.json();
        
        // ✅ Only update state if not aborted
        setResults(data);
      } catch (error) {
        if (error.name === 'AbortError') {
          console.log('Search aborted for:', query);
          return; // Don't update state on abort
        }
        console.error('Search error:', error);
      } finally {
        setLoading(false);
      }
    };

    searchProducts();

    // ✅ Cleanup - abort previous search when query changes
    return () => {
      abortController.abort();
    };
  }, [query]); // New query = abort previous search

  return (
    <div>
      <input 
        value={query} 
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search..."
      />
      {loading && <p>Searching...</p>}
      <ul>{results.map(r => <li key={r.id}>{r.name}</li>)}</ul>
    </div>
  );
};

Example: Request ID Tracking

const UserProfile = ({ userId }) => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    let isCurrent = true; // ✅ Flag to track if this request is current
    let requestId = Symbol(); // Unique ID for this request

    const fetchUser = async () => {
      setLoading(true);
      
      try {
        // Simulate network delay
        await new Promise(resolve => setTimeout(resolve, 
          Math.random() * 2000)); // Random delay 0-2s
        
        const response = await fetch(`/api/users/${userId}`);
        const data = await response.json();
        
        // ✅ Only update if this is still the current request
        if (isCurrent) {
          setUser(data);
          console.log('Updated user:', userId, requestId.toString());
        } else {
          console.log('Ignored stale response for:', userId, requestId.toString());
        }
      } catch (error) {
        if (isCurrent) {
          console.error('Error fetching user:', error);
        }
      } finally {
        if (isCurrent) {
          setLoading(false);
        }
      }
    };

    fetchUser();

    // ✅ Cleanup - mark request as stale
    return () => {
      isCurrent = false;
      console.log('Marking stale:', userId, requestId.toString());
    };
  }, [userId]); // New userId = previous request marked stale

  if (loading) return <p>Loading...</p>;
  return <div>{user?.name}</div>;
};

Example: Debounced Search with Race Condition Prevention

const useDebounce = (value, delay) => {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);

  return debouncedValue;
};

const DebouncedSearch = () => {
  const [searchTerm, setSearchTerm] = useState('');
  const [results, setResults] = useState([]);
  const debouncedSearch = useDebounce(searchTerm, 300); // 300ms delay

  useEffect(() => {
    if (!debouncedSearch) {
      setResults([]);
      return;
    }

    const abortController = new AbortController();

    const performSearch = async () => {
      try {
        const response = await fetch(
          `/api/search?q=${debouncedSearch}`,
          { signal: abortController.signal }
        );
        const data = await response.json();
        setResults(data);
      } catch (error) {
        if (error.name !== 'AbortError') {
          console.error('Search error:', error);
        }
      }
    };

    performSearch();

    // ✅ Double protection: debounce + abort
    return () => abortController.abort();
  }, [debouncedSearch]); // Only search when debounced value changes

  return (
    <div>
      <input 
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
        placeholder="Type to search..."
      />
      <p>Searching for: {debouncedSearch}</p>
      <ul>{results.map(r => <li key={r.id}>{r.name}</li>)}</ul>
    </div>
  );
};
Race Condition Warning: Never rely on the order of async operations completing. Always use AbortController, request ID tracking, or cleanup flags to ensure only the latest or relevant responses update state. Test with artificial delays to expose race conditions during development.

State and Side Effects Best Practices Summary:

  • Always cleanup - Return cleanup function from useEffect for subscriptions/timers
  • Abort requests - Use AbortController to cancel stale async requests
  • Track mount status - Prevent state updates on unmounted components
  • Debounce inputs - Reduce unnecessary API calls for rapid user input
  • Handle race conditions - Use request IDs or abort signals
  • Sync carefully - Be cautious with infinite loops in useEffect
  • Use proper deps - Include all values used in effect to avoid stale closures
  • Test cleanup - Verify cleanup functions prevent memory leaks