What are your preferred patterns for handling side effects in React?
3 minintermediatereactbest-practicespreferredpatternshandlingside
Quick Answer
Key aspects: useEffect Hook Patterns; Custom Hooks for Reusable Side Effects; Event Listeners and Cleanup; Subscription Management; Advanced Patterns with useReducer; Best Practices.
Detailed Answer
What are your preferred patterns for handling side effects in React?
Answer:
1. useEffect Hook Patterns:
// Basic data fetching pattern
const useUserData = (userId) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
const fetchUser = async () => {
try {
setLoading(true);
setError(null);
const userData = await api.getUser(userId);
if (!cancelled) {
setUser(userData);
}
} catch (err) {
if (!cancelled) {
setError(err.message);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
};
fetchUser();
return () => {
cancelled = true; // Cleanup to prevent state updates on unmounted component
};
}, [userId]);
return { user, loading, error };
};
2. Custom Hooks for Reusable Side Effects:
// Debounced search hook
const useDebounce = (value, delay) => {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
};
// Usage
const SearchComponent = () => {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebounce(searchTerm, 300);
const { results, loading } = useSearchResults(debouncedSearchTerm);
return (
<div>
<input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search..."
/>
{loading && <div>Searching...</div>}
{results.map(result => <div key={result.id}>{result.title}</div>)}
</div>
);
};
3. Event Listeners and Cleanup:
const useWindowSize = () => {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return windowSize;
};
4. Subscription Management:
const useWebSocket = (url) => {
const [socket, setSocket] = useState(null);
const [lastMessage, setLastMessage] = useState(null);
const [connectionStatus, setConnectionStatus] = useState('disconnected');
useEffect(() => {
const ws = new WebSocket(url);
ws.onopen = () => {
setConnectionStatus('connected');
setSocket(ws);
};
ws.onmessage = (event) => {
setLastMessage(JSON.parse(event.data));
};
ws.onclose = () => {
setConnectionStatus('disconnected');
setSocket(null);
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
setConnectionStatus('error');
};
return () => {
ws.close();
};
}, [url]);
const sendMessage = useCallback((message) => {
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify(message));
}
}, [socket]);
return { lastMessage, connectionStatus, sendMessage };
};
5. Advanced Patterns with useReducer:
const dataFetchReducer = (state, action) => {
switch (action.type) {
case 'FETCH_INIT':
return {
...state,
loading: true,
error: null,
};
case 'FETCH_SUCCESS':
return {
...state,
loading: false,
data: action.payload,
error: null,
};
case 'FETCH_FAILURE':
return {
...state,
loading: false,
error: action.payload,
};
default:
throw new Error(`Unhandled action type: ${action.type}`);
}
};
const useDataFetcher = (initialUrl) => {
const [url, setUrl] = useState(initialUrl);
const [state, dispatch] = useReducer(dataFetchReducer, {
data: null,
loading: false,
error: null,
});
useEffect(() => {
let cancelled = false;
const fetchData = async () => {
dispatch({ type: 'FETCH_INIT' });
try {
const result = await fetch(url);
const data = await result.json();
if (!cancelled) {
dispatch({ type: 'FETCH_SUCCESS', payload: data });
}
} catch (error) {
if (!cancelled) {
dispatch({ type: 'FETCH_FAILURE', payload: error.message });
}
}
};
if (url) {
fetchData();
}
return () => {
cancelled = true;
};
}, [url]);
return { ...state, setUrl };
};
6. Best Practices:
- Always provide cleanup functions to prevent memory leaks
- Use dependency arrays correctly to avoid infinite loops
- Extract complex side effects into custom hooks for reusability
- Handle loading and error states consistently
- Use useCallback and useMemo to prevent unnecessary re-renders
- Consider using libraries like React Query for complex data fetching scenarios