Based on the React documentation "You Might Not Need an Effect", here are the core principles:
- Don't use Effects to transform data for rendering - Calculate during render instead
- Don't use Effects to handle user events - Use event handlers instead
- Don't use Effects for derived state - Calculate from existing props/state during render
- Don't use Effects to reset state on prop changes - Use the
keyprop instead
Effects should only be used for synchronizing with external systems:
- Network requests
- Browser APIs (DOM manipulation, timers)
- Third-party libraries
- Subscriptions and cleanup
useEffect(() => {
if (!isSupported || !swManager) {
setIsLoading(false);
return;
}
// ... service worker initialization
}, []);✅ Correct: Synchronizing with external system (Service Worker API)
useEffect(() => {
if (!isManuallyClosedRef.current) {
open();
}
return () => {
close();
};
}, []);✅ Correct: Managing external connection lifecycle
React.useEffect(() => {
if (typeof window === 'undefined') return;
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
};
mql.addEventListener('change', onChange);
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
return () => mql.removeEventListener('change', onChange);
}, []);✅ Correct: Synchronizing with browser API (matchMedia)
// CURRENT - Potentially unnecessary
const [showOnboarding, setShowOnboarding] = useState(false);
const hasApiKeys = Object.keys(apiKeys).some(/*...*/);
useEffect(() => {
if (!hasApiKeys) {
setShowOnboarding(true);
}
}, [hasApiKeys]);🔧 IMPROVED - Derived state:
export function useRagOnboarding() {
const { getAllKeys } = useApiKeysStore();
const apiKeys = getAllKeys();
// Calculate derived state during render
const hasApiKeys = Object.keys(apiKeys).some(
key => key === API_KEY_NAMES.GOOGLE || key === API_KEY_NAMES.OPENAI,
);
// Derive showOnboarding directly from hasApiKeys
const showOnboarding = !hasApiKeys;
return {
showOnboarding,
hasApiKeys,
completeOnboarding: () => {}, // Could be handled by parent
skipOnboarding: () => {}, // Could be handled by parent
};
}// CURRENT - Potentially unnecessary
const [_debouncedStatus, setDebouncedStatus] = useState(threadItem.status);
const [_debouncedError, setDebouncedError] = useState(threadItem.error);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedStatus(threadItem.status);
setDebouncedError(threadItem.error);
}, 50);
return () => clearTimeout(timer);
}, [threadItem.status, threadItem.error]);🔧 IMPROVED - Custom hook with useMemo:
function useDebounced<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// In component:
const debouncedStatus = useDebounced(threadItem.status, 50);
const debouncedError = useDebounced(threadItem.error, 50);// CURRENT - Complex effect logic
useEffect(() => {
if (
isSupported
&& !isInstalled
&& (deferredPrompt || isIOS)
&& isHomepage
&& !bannerDismissed
) {
setShowBanner(true);
const timer = setTimeout(() => {
setShowBanner(false);
setBannerDismissed(true);
}, 4000);
return () => clearTimeout(timer);
} else {
setShowBanner(false);
}
}, [isSupported, isInstalled, deferredPrompt, isIOS, isHomepage, bannerDismissed]);🔧 IMPROVED - Separate concerns:
// Derive banner visibility
const shouldShowBanner = isSupported && !isInstalled && (deferredPrompt || isIOS) && isHomepage
&& !bannerDismissed;
// Separate effect for auto-dismiss timer
useEffect(() => {
if (!shouldShowBanner) return;
const timer = setTimeout(() => {
setBannerDismissed(true);
}, 4000);
return () => clearTimeout(timer);
}, [shouldShowBanner]);
// Use derived state for showBanner
const showBanner = shouldShowBanner;The chat store has complex initialization logic that could be simplified by moving more logic to derived state and reducing effect dependencies.
The ThreadItem component has several effects that could be optimized:
- Source extraction could be derived state
- Error toast logic could be moved to event handlers
- View tracking could use a custom hook
- Replace effects that update state based on props/state with calculated values
- Use
useMemofor expensive calculations - Remove unnecessary state variables
- Combine related effects where possible
- Extract complex effect logic into custom hooks
- Separate concerns (data fetching vs. UI updates)
- Move user interaction logic from effects to event handlers
- Use callbacks for state updates triggered by user actions
- Minimize effect dependencies
- Use
useCallbackanduseMemoto stabilize dependencies - Consider using refs for values that don't need to trigger re-renders
- ✅ Fix
useRagOnboardinghook - converted to derived state - ✅ Optimize PWA banner logic - separated concerns, removed unnecessary state
- ✅ Review thread item effects - extracted error toast hook, optimized sources extraction
- ✅ Create reusable
useDebouncedhook for common pattern - ✅ Create reusable
useErrorToasthook for consistent error handling
- Audit chat store initialization - complex state management could be simplified
- Review all custom hooks for unnecessary effects
- Optimize component re-renders with better memoization
- Consider extracting more reusable patterns from remaining effects
- Performance audit with React DevTools
- Add ESLint rules for effect best practices
- Create additional reusable effect patterns/hooks
Most effects in the codebase are actually following best practices:
- DOM Synchronization: Focus management, scroll behavior, event listeners
- External API Integration: Service worker registration, network requests
- Browser API Synchronization: Media queries, storage events
- Cleanup Patterns: Proper cleanup of timers, event listeners, subscriptions
- Chat Store: Complex initialization logic could potentially be simplified
- Component Memoization: Some components could benefit from better memoization
- State Derivation: Look for more cases where computed values are stored in state
- Think "derived state first" - Can this be calculated from existing state?
- Question every effect - Is this really synchronizing with an external system?
- Prefer event handlers - For user interactions, use event handlers
- Use the
keyprop - For resetting component state - Extract custom hooks - For reusable effect logic
- Minimize dependencies - Keep effect dependency arrays small and stable