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