Component Lifecycle and Effects

1. useEffect Hook Patterns and Dependencies

Pattern Dependencies Runs When Use Case
No dependencies useEffect(() => {}) Every render Rarely needed, usually a mistake
Empty array useEffect(() => {}, []) Once on mount Initial setup, subscriptions
With dependencies useEffect(() => {}, [a, b]) When a or b changes Sync with specific values
Single dependency useEffect(() => {}, [id]) When id changes Fetch data for specific ID
Object dependency useEffect(() => {}, [obj]) When object reference changes Can cause extra renders
Function dependency useEffect(() => {}, [fn]) When function reference changes Needs useCallback wrapper
Dependency Rule Description ESLint Rule
Include all used values List all props, state, vars used in effect exhaustive-deps
Primitive values Numbers, strings, booleans - safe No warning
Object/array deps New reference = triggers effect May need useMemo
Function deps Wrap with useCallback May need useCallback
Refs don't trigger ref.current changes ignored Safe to omit
setState stable setState functions don't change Safe to omit

Example: useEffect dependency patterns

// Run once on mount (componentDidMount equivalent)
useEffect(() => {
  console.log('Component mounted');
  document.title = 'My App';
}, []); // Empty dependency array

// Run on every render (usually wrong!)
useEffect(() => {
  console.log('Every render'); // Performance issue!
}); // No dependency array - avoid this

// Run when specific values change
useEffect(() => {
  console.log(\`Count changed to \${count}\`);
}, [count]); // Re-runs when count changes

// Multiple dependencies
useEffect(() => {
  console.log(\`User \${userId} on page \${page}\`);
  fetchUserData(userId, page);
}, [userId, page]); // Re-runs when either changes

// Derived values in effect
const UserProfile = ({ userId }) => {
  const [user, setUser] = useState(null);
  
  useEffect(() => {
    let cancelled = false;
    
    const fetchUser = async () => {
      const data = await fetch(\`/api/users/\${userId}\`).then(r => r.json());
      if (!cancelled) {
        setUser(data);
      }
    };
    
    fetchUser();
    
    return () => {
      cancelled = true;
    };
  }, [userId]); // Only userId dependency needed
  
  return user ? <div>{user.name}</div> : <div>Loading...</div>;
};

// Object dependency issue
const BadObjectDep = ({ config }) => {
  useEffect(() => {
    // This runs every render if config is recreated!
    console.log(config.apiUrl);
  }, [config]); // ❌ Object reference changes each render
  
  return null;
};

// Fix: Extract primitive values
const GoodObjectDep = ({ config }) => {
  useEffect(() => {
    console.log(config.apiUrl);
  }, [config.apiUrl]); // ✅ Only re-run if apiUrl changes
  
  return null;
};

// Function dependency issue
const Parent = () => {
  const [count, setCount] = useState(0);
  
  // ❌ New function every render
  const handleClick = () => {
    console.log(count);
  };
  
  return <Child onClick={handleClick} />;
};

const Child = ({ onClick }) => {
  useEffect(() => {
    // Runs every render because onClick changes
    console.log('onClick changed');
  }, [onClick]); // ❌ Function reference changes
  
  return <button onClick={onClick}>Click</button>;
};

// Fix: Use useCallback
const ParentFixed = () => {
  const [count, setCount] = useState(0);
  
  // ✅ Stable function reference
  const handleClick = useCallback(() => {
    console.log(count);
  }, [count]);
  
  return <ChildFixed onClick={handleClick} />;
};

// setState doesn't need to be in dependencies
const Counter = () => {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    const timer = setInterval(() => {
      setCount(c => c + 1); // ✅ Functional update
    }, 1000);
    
    return () => clearInterval(timer);
  }, []); // setCount not needed in deps
  
  return <div>{count}</div>;
};
Warning: React's ESLint plugin (eslint-plugin-react-hooks) will warn about missing dependencies. Don't ignore these warnings - they prevent bugs. If you intentionally want to omit a dependency, use // eslint-disable-next-line react-hooks/exhaustive-deps with a comment explaining why.

2. Effect Cleanup and Memory Leak Prevention

Cleanup Scenario Without Cleanup With Cleanup Memory Leak Risk
Timers Continues running after unmount clearTimeout/Interval High
Event listeners Accumulate on every mount removeEventListener High
Subscriptions Connection stays open unsubscribe() High
Async operations setState on unmounted component Cancellation flag/AbortController Medium
WebSocket Connection remains open socket.close() High
Animation frames Continues running cancelAnimationFrame Medium
Observers Observers keep watching observer.disconnect() High

Example: Effect cleanup patterns

// Timer cleanup
const Timer = () => {
  const [seconds, setSeconds] = useState(0);
  
  useEffect(() => {
    const interval = setInterval(() => {
      setSeconds(s => s + 1);
    }, 1000);
    
    // Cleanup function
    return () => {
      clearInterval(interval);
      console.log('Timer cleaned up');
    };
  }, []);
  
  return <div>Seconds: {seconds}</div>;
};

// Event listener cleanup
const WindowSize = () => {
  const [size, setSize] = useState({ width: 0, height: 0 });
  
  useEffect(() => {
    const handleResize = () => {
      setSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    };
    
    // Add listener
    window.addEventListener('resize', handleResize);
    handleResize(); // Initial call
    
    // Cleanup: remove listener
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);
  
  return <div>{size.width} x {size.height}</div>;
};

// Async operation cleanup (prevent setState on unmounted)
const UserData = ({ userId }) => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    let cancelled = false; // Cancellation flag
    
    const fetchUser = async () => {
      setLoading(true);
      
      try {
        const response = await fetch(\`/api/users/\${userId}\`);
        const data = await response.json();
        
        // Only update state if not cancelled
        if (!cancelled) {
          setUser(data);
          setLoading(false);
        }
      } catch (error) {
        if (!cancelled) {
          console.error(error);
          setLoading(false);
        }
      }
    };
    
    fetchUser();
    
    // Cleanup: set flag to prevent setState
    return () => {
      cancelled = true;
    };
  }, [userId]);
  
  if (loading) return <div>Loading...</div>;
  return <div>{user?.name}</div>;
};

// AbortController for fetch cleanup (modern approach)
const DataFetcher = ({ endpoint }) => {
  const [data, setData] = useState(null);
  
  useEffect(() => {
    const controller = new AbortController();
    
    const fetchData = async () => {
      try {
        const response = await fetch(endpoint, {
          signal: controller.signal
        });
        const result = await response.json();
        setData(result);
      } catch (error) {
        if (error.name === 'AbortError') {
          console.log('Fetch aborted');
        } else {
          console.error(error);
        }
      }
    };
    
    fetchData();
    
    // Cleanup: abort fetch
    return () => {
      controller.abort();
    };
  }, [endpoint]);
  
  return <div>{JSON.stringify(data)}</div>;
};

// WebSocket cleanup
const LiveChat = ({ roomId }) => {
  const [messages, setMessages] = useState([]);
  
  useEffect(() => {
    const ws = new WebSocket(\`wss://chat.example.com/\${roomId}\`);
    
    ws.onopen = () => {
      console.log('Connected to chat');
    };
    
    ws.onmessage = (event) => {
      const message = JSON.parse(event.data);
      setMessages(prev => [...prev, message]);
    };
    
    ws.onerror = (error) => {
      console.error('WebSocket error:', error);
    };
    
    // Cleanup: close WebSocket
    return () => {
      ws.close();
      console.log('Disconnected from chat');
    };
  }, [roomId]);
  
  return (
    <div>
      {messages.map((msg, i) => (
        <div key={i}>{msg.text}</div>
      ))}
    </div>
  );
};

// IntersectionObserver cleanup
const LazyImage = ({ src, alt }) => {
  const [isVisible, setIsVisible] = useState(false);
  const imgRef = useRef(null);
  
  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setIsVisible(true);
          observer.disconnect(); // Stop observing once visible
        }
      },
      { threshold: 0.1 }
    );
    
    if (imgRef.current) {
      observer.observe(imgRef.current);
    }
    
    // Cleanup: disconnect observer
    return () => {
      observer.disconnect();
    };
  }, []);
  
  return (
    <div ref={imgRef}>
      {isVisible ? (
        <img src={src} alt={alt} />
      ) : (
        <div>Loading...</div>
      )}
    </div>
  );
};

// Animation frame cleanup
const AnimatedCounter = ({ target }) => {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    let frame;
    let startTime = null;
    
    const animate = (timestamp) => {
      if (!startTime) startTime = timestamp;
      const progress = timestamp - startTime;
      
      const nextCount = Math.min(
        Math.floor((progress / 2000) * target),
        target
      );
      
      setCount(nextCount);
      
      if (nextCount < target) {
        frame = requestAnimationFrame(animate);
      }
    };
    
    frame = requestAnimationFrame(animate);
    
    // Cleanup: cancel animation
    return () => {
      cancelAnimationFrame(frame);
    };
  }, [target]);
  
  return <div>{count}</div>;
};
Warning: Always clean up side effects! Common memory leaks: forgetting to clear timers, not removing event listeners, leaving WebSockets open, or calling setState on unmounted components.

3. Effect Timing and useLayoutEffect

Hook Timing Blocks Paint Use Case
useEffect After paint (async) No Most side effects, data fetching
useLayoutEffect Before paint (sync) Yes DOM measurements, prevent flicker
Execution Order Phase Description
1. Render Component function runs JSX returned, state/props evaluated
2. React updates DOM DOM mutations Changes applied to DOM
3. useLayoutEffect Before browser paint Synchronous, blocks painting
4. Browser paints Visual update User sees changes
5. useEffect After paint Async, doesn't block paint

Example: useEffect vs useLayoutEffect timing

// useEffect - runs AFTER paint (preferred)
const DataComponent = ({ id }) => {
  const [data, setData] = useState(null);
  
  useEffect(() => {
    // Runs after component is painted
    // Good for data fetching - doesn't block UI
    fetchData(id).then(setData);
  }, [id]);
  
  return <div>{data?.name}</div>;
};

// useLayoutEffect - runs BEFORE paint
const TooltipPosition = () => {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  const tooltipRef = useRef(null);
  
  useLayoutEffect(() => {
    // Measure DOM before browser paints
    if (tooltipRef.current) {
      const rect = tooltipRef.current.getBoundingClientRect();
      
      // Adjust position if off screen
      let x = rect.left;
      let y = rect.top;
      
      if (x + rect.width > window.innerWidth) {
        x = window.innerWidth - rect.width - 10;
      }
      
      if (y + rect.height > window.innerHeight) {
        y = window.innerHeight - rect.height - 10;
      }
      
      setPosition({ x, y });
    }
  }); // Runs on every render
  
  return (
    <div
      ref={tooltipRef}
      style={{ left: position.x, top: position.y }}
    >
      Tooltip
    </div>
  );
};

// Prevent visual flicker with useLayoutEffect
const AnimatedHeight = ({ isOpen, children }) => {
  const [height, setHeight] = useState(0);
  const contentRef = useRef(null);
  
  useLayoutEffect(() => {
    if (isOpen && contentRef.current) {
      // Measure and set height BEFORE paint
      // Prevents flicker from 0 to auto height
      const contentHeight = contentRef.current.scrollHeight;
      setHeight(contentHeight);
    } else {
      setHeight(0);
    }
  }, [isOpen]);
  
  return (
    <div
      style={{
        height: height,
        overflow: 'hidden',
        transition: 'height 0.3s ease'
      }}
    >
      <div ref={contentRef}>{children}</div>
    </div>
  );
};

// DOM mutation before paint
const ScrollToTop = ({ trigger }) => {
  useLayoutEffect(() => {
    // Scroll happens before user sees the page
    window.scrollTo(0, 0);
  }, [trigger]);
  
  return null;
};

// Comparison: flicker with useEffect
const FlickerExample = () => {
  const [width, setWidth] = useState(0);
  const ref = useRef(null);
  
  // ❌ WRONG: causes visible flicker
  useEffect(() => {
    // Runs AFTER paint - user sees wrong width first
    if (ref.current) {
      setWidth(ref.current.offsetWidth);
    }
  }, []);
  
  return <div ref={ref}>Width: {width}px</div>;
};

// Fixed: no flicker with useLayoutEffect
const NoFlickerExample = () => {
  const [width, setWidth] = useState(0);
  const ref = useRef(null);
  
  // ✅ CORRECT: measures before paint
  useLayoutEffect(() => {
    // Runs BEFORE paint - correct width from start
    if (ref.current) {
      setWidth(ref.current.offsetWidth);
    }
  }, []);
  
  return <div ref={ref}>Width: {width}px</div>;
};

// Focus management with useLayoutEffect
const AutoFocus = ({ shouldFocus }) => {
  const inputRef = useRef(null);
  
  useLayoutEffect(() => {
    if (shouldFocus && inputRef.current) {
      // Focus before paint - no visual jump
      inputRef.current.focus();
    }
  }, [shouldFocus]);
  
  return <input ref={inputRef} />;
};
Rule of thumb: Use useEffect for 99% of cases. Only use useLayoutEffect when you need to read layout (measurements, scroll position) or prevent visual flicker. useLayoutEffect can hurt performance because it blocks painting.

4. Data Fetching with useEffect Patterns

Pattern Best Practice Issues to Handle
Basic fetch async/await in effect Loading state, errors
Cleanup Cancellation flag or AbortController setState on unmounted component
Dependencies Include all params used in fetch Stale data, race conditions
Race conditions Ignore results from outdated requests Wrong data displayed
Error handling Try/catch and error state Unhandled rejections
Loading state Boolean flag while fetching Poor UX without feedback

Example: Data fetching patterns

// Basic data fetching
const UserProfile = ({ userId }) => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    const fetchUser = async () => {
      try {
        setLoading(true);
        setError(null);
        
        const response = await fetch(\`/api/users/\${userId}\`);
        
        if (!response.ok) {
          throw new Error('Failed to fetch user');
        }
        
        const data = await response.json();
        setUser(data);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };
    
    fetchUser();
  }, [userId]); // Re-fetch 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>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
};

// With cleanup to prevent setState on unmounted
const DataFetchWithCleanup = ({ endpoint }) => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    let cancelled = false;
    
    const fetchData = async () => {
      try {
        const response = await fetch(endpoint);
        const result = await response.json();
        
        if (!cancelled) {
          setData(result);
          setLoading(false);
        }
      } catch (error) {
        if (!cancelled) {
          console.error(error);
          setLoading(false);
        }
      }
    };
    
    fetchData();
    
    return () => {
      cancelled = true;
    };
  }, [endpoint]);
  
  return loading ? <div>Loading...</div> : <div>{JSON.stringify(data)}</div>;
};

// Race condition handling
const SearchResults = ({ query }) => {
  const [results, setResults] = useState([]);
  const [loading, setLoading] = useState(false);
  
  useEffect(() => {
    if (!query) {
      setResults([]);
      return;
    }
    
    let cancelled = false;
    setLoading(true);
    
    const search = async () => {
      try {
        const response = await fetch(\`/api/search?q=\${query}\`);
        const data = await response.json();
        
        // Only update if this is still the latest query
        if (!cancelled) {
          setResults(data);
          setLoading(false);
        }
      } catch (error) {
        if (!cancelled) {
          console.error(error);
          setLoading(false);
        }
      }
    };
    
    search();
    
    // Cleanup: ignore results if query changes
    return () => {
      cancelled = true;
    };
  }, [query]);
  
  return (
    <div>
      {loading && <div>Searching...</div>}
      <ul>
        {results.map(r => (
          <li key={r.id}>{r.title}</li>
        ))}
      </ul>
    </div>
  );
};

// AbortController pattern (modern)
const ModernFetch = ({ url }) => {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    const controller = new AbortController();
    
    const fetchData = async () => {
      try {
        const response = await fetch(url, {
          signal: controller.signal
        });
        
        const result = await response.json();
        setData(result);
      } catch (err) {
        // Don't set error if aborted
        if (err.name !== 'AbortError') {
          setError(err.message);
        }
      }
    };
    
    fetchData();
    
    return () => {
      controller.abort();
    };
  }, [url]);
  
  if (error) return <div>Error: {error}</div>;
  return <div>{JSON.stringify(data)}</div>;
};

// Debounced search
const DebouncedSearch = () => {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [loading, setLoading] = useState(false);
  
  useEffect(() => {
    if (!query) {
      setResults([]);
      return;
    }
    
    setLoading(true);
    
    // Debounce: wait 500ms after user stops typing
    const timer = setTimeout(async () => {
      try {
        const response = await fetch(\`/api/search?q=\${query}\`);
        const data = await response.json();
        setResults(data);
      } catch (error) {
        console.error(error);
      } finally {
        setLoading(false);
      }
    }, 500);
    
    // Cleanup: cancel previous timeout
    return () => {
      clearTimeout(timer);
    };
  }, [query]);
  
  return (
    <div>
      <input
        value={query}
        onChange={e => setQuery(e.target.value)}
        placeholder="Search..."
      />
      {loading && <div>Searching...</div>}
      <ul>
        {results.map(r => (
          <li key={r.id}>{r.title}</li>
        ))}
      </ul>
    </div>
  );
};

// Multiple parallel requests
const Dashboard = () => {
  const [user, setUser] = useState(null);
  const [posts, setPosts] = useState([]);
  const [notifications, setNotifications] = useState([]);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    const fetchAll = async () => {
      try {
        // Fetch all in parallel
        const [userRes, postsRes, notifRes] = await Promise.all([
          fetch('/api/user'),
          fetch('/api/posts'),
          fetch('/api/notifications')
        ]);
        
        const [userData, postsData, notifData] = await Promise.all([
          userRes.json(),
          postsRes.json(),
          notifRes.json()
        ]);
        
        setUser(userData);
        setPosts(postsData);
        setNotifications(notifData);
      } catch (error) {
        console.error(error);
      } finally {
        setLoading(false);
      }
    };
    
    fetchAll();
  }, []);
  
  if (loading) return <div>Loading dashboard...</div>;
  
  return (
    <div>
      <h1>Welcome {user?.name}</h1>
      <div>Posts: {posts.length}</div>
      <div>Notifications: {notifications.length}</div>
    </div>
  );
};
Warning: For production apps, consider using dedicated data fetching libraries like SWR or React Query instead of manual useEffect. They handle caching, revalidation, race conditions, and optimistic updates automatically.

5. Custom Hooks for Effect Logic

Custom Hook Encapsulates Returns Reusability
useFetch Data fetching logic {data, loading, error} High - any API endpoint
useDebounce Debounced value updates Debounced value High - search, filters
useInterval Timer with cleanup void Medium - polling
useEventListener Event subscription void High - any event
useLocalStorage localStorage sync [value, setValue] High - persistence
useWindowSize Window dimensions {width, height} High - responsive

Example: Reusable custom hooks

// useFetch - reusable data fetching
const useFetch = (url) => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    if (!url) return;
    
    const controller = new AbortController();
    
    const fetchData = async () => {
      try {
        setLoading(true);
        const response = await fetch(url, {
          signal: controller.signal
        });
        
        if (!response.ok) {
          throw new Error(\`HTTP error! status: \${response.status}\`);
        }
        
        const result = await response.json();
        setData(result);
        setError(null);
      } catch (err) {
        if (err.name !== 'AbortError') {
          setError(err.message);
        }
      } finally {
        setLoading(false);
      }
    };
    
    fetchData();
    
    return () => controller.abort();
  }, [url]);
  
  return { data, loading, error };
};

// Usage
const UserProfile = ({ userId }) => {
  const { data: user, loading, error } = useFetch(\`/api/users/\${userId}\`);
  
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  return <div>{user.name}</div>;
};

// useDebounce - delay value updates
const useDebounce = (value, delay) => {
  const [debouncedValue, setDebouncedValue] = useState(value);
  
  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);
    
    return () => clearTimeout(timer);
  }, [value, delay]);
  
  return debouncedValue;
};

// Usage
const SearchComponent = () => {
  const [search, setSearch] = useState('');
  const debouncedSearch = useDebounce(search, 500);
  const { data } = useFetch(\`/api/search?q=\${debouncedSearch}\`);
  
  return (
    <div>
      <input value={search} onChange={e => setSearch(e.target.value)} />
      <div>{JSON.stringify(data)}</div>
    </div>
  );
};

// useInterval - declarative setInterval
const useInterval = (callback, delay) => {
  const savedCallback = useRef();
  
  // Remember latest callback
  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);
  
  // Set up interval
  useEffect(() => {
    if (delay === null) return;
    
    const tick = () => {
      savedCallback.current();
    };
    
    const id = setInterval(tick, delay);
    return () => clearInterval(id);
  }, [delay]);
};

// Usage
const Clock = () => {
  const [time, setTime] = useState(new Date());
  
  useInterval(() => {
    setTime(new Date());
  }, 1000);
  
  return <div>{time.toLocaleTimeString()}</div>;
};

// useEventListener - declarative addEventListener
const useEventListener = (eventName, handler, element = window) => {
  const savedHandler = useRef();
  
  useEffect(() => {
    savedHandler.current = handler;
  }, [handler]);
  
  useEffect(() => {
    const isSupported = element && element.addEventListener;
    if (!isSupported) return;
    
    const eventListener = (event) => savedHandler.current(event);
    element.addEventListener(eventName, eventListener);
    
    return () => {
      element.removeEventListener(eventName, eventListener);
    };
  }, [eventName, element]);
};

// Usage
const KeyLogger = () => {
  const [key, setKey] = useState('');
  
  useEventListener('keydown', (e) => {
    setKey(e.key);
  });
  
  return <div>Last key: {key}</div>;
};

// useLocalStorage - sync state with localStorage
const useLocalStorage = (key, initialValue) => {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(error);
      return initialValue;
    }
  });
  
  const setValue = (value) => {
    try {
      const valueToStore = value instanceof Function
        ? value(storedValue)
        : value;
      
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.error(error);
    }
  };
  
  return [storedValue, setValue];
};

// Usage
const ThemeToggle = () => {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  
  return (
    <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
      Current: {theme}
    </button>
  );
};

// useWindowSize - track window dimensions
const useWindowSize = () => {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });
  
  useEffect(() => {
    const handleResize = () => {
      setSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    };
    
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);
  
  return size;
};

// Usage
const ResponsiveComponent = () => {
  const { width } = useWindowSize();
  
  return (
    <div>
      {width < 768 ? 'Mobile' : 'Desktop'} - {width}px
    </div>
  );
};
Note: Custom hooks let you extract and reuse effect logic across components. Name them with use prefix and follow all hooks rules. They can use other hooks internally.

6. Effect Performance and Optimization

Optimization Technique Impact When to Use
Proper dependencies Only include what changes Avoid unnecessary runs Always
useMemo deps Memoize object/array deps Prevent extra effect runs Object dependencies
useCallback deps Memoize function deps Stable function reference Function dependencies
Split effects One effect per concern Run only what's needed Multiple responsibilities
Functional updates setState(prev => ...) Remove state from deps State-dependent updates
Debouncing Delay effect execution Reduce frequency Frequent changes (input)
Throttling Limit execution rate Control frequency Continuous events (scroll)

Example: Effect performance optimizations

// ❌ BAD: Unnecessary effect runs
const BadDeps = ({ user }) => {
  useEffect(() => {
    console.log(user.name);
  }, [user]); // Runs every time user object changes (even if name is same)
  
  return null;
};

// ✅ GOOD: Only depend on what you use
const GoodDeps = ({ user }) => {
  useEffect(() => {
    console.log(user.name);
  }, [user.name]); // Only runs when name actually changes
  
  return null;
};

// ❌ BAD: Object dependency causes extra runs
const ParentBad = () => {
  const [count, setCount] = useState(0);
  
  // New object every render!
  const config = { apiUrl: 'https://api.example.com' };
  
  return <ChildBad config={config} />;
};

const ChildBad = ({ config }) => {
  useEffect(() => {
    fetch(config.apiUrl); // Runs every render!
  }, [config]);
  
  return null;
};

// ✅ GOOD: Memoize object dependency
const ParentGood = () => {
  const [count, setCount] = useState(0);
  
  // Same object reference across renders
  const config = useMemo(
    () => ({ apiUrl: 'https://api.example.com' }),
    []
  );
  
  return <ChildGood config={config} />;
};

const ChildGood = ({ config }) => {
  useEffect(() => {
    fetch(config.apiUrl); // Only runs once
  }, [config]);
  
  return null;
};

// ❌ BAD: Multiple concerns in one effect
const BadMultiEffect = ({ userId, theme }) => {
  useEffect(() => {
    // Fetch user data
    fetchUser(userId).then(setUser);
    
    // Apply theme
    document.body.className = theme;
    
    // Both run on every userId OR theme change!
  }, [userId, theme]);
  
  return null;
};

// ✅ GOOD: Split into separate effects
const GoodMultiEffect = ({ userId, theme }) => {
  // Effect 1: User data (only re-runs when userId changes)
  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, [userId]);
  
  // Effect 2: Theme (only re-runs when theme changes)
  useEffect(() => {
    document.body.className = theme;
  }, [theme]);
  
  return null;
};

// ❌ BAD: State in dependencies
const BadStateDep = () => {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    const timer = setInterval(() => {
      setCount(count + 1); // Uses stale count
    }, 1000);
    
    return () => clearInterval(timer);
  }, [count]); // Re-creates interval on every count change!
  
  return <div>{count}</div>;
};

// ✅ GOOD: Functional update removes dependency
const GoodStateDep = () => {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    const timer = setInterval(() => {
      setCount(c => c + 1); // Functional update
    }, 1000);
    
    return () => clearInterval(timer);
  }, []); // No dependencies - interval created once
  
  return <div>{count}</div>;
};

// Debouncing to reduce effect frequency
const SearchWithDebounce = () => {
  const [searchTerm, setSearchTerm] = useState('');
  const [results, setResults] = useState([]);
  
  useEffect(() => {
    // Wait for user to stop typing
    const timer = setTimeout(() => {
      if (searchTerm) {
        fetch(\`/api/search?q=\${searchTerm}\`)
          .then(r => r.json())
          .then(setResults);
      }
    }, 500); // Debounce 500ms
    
    return () => clearTimeout(timer);
  }, [searchTerm]);
  
  return (
    <div>
      <input
        value={searchTerm}
        onChange={e => setSearchTerm(e.target.value)}
      />
      <ul>
        {results.map(r => (
          <li key={r.id}>{r.title}</li>
        ))}
      </ul>
    </div>
  );
};

// Throttling for continuous events
const ScrollTracker = () => {
  const [scrollY, setScrollY] = useState(0);
  
  useEffect(() => {
    let ticking = false;
    
    const handleScroll = () => {
      if (!ticking) {
        window.requestAnimationFrame(() => {
          setScrollY(window.scrollY);
          ticking = false;
        });
        
        ticking = true;
      }
    };
    
    window.addEventListener('scroll', handleScroll);
    return () => window.removeEventListener('scroll', handleScroll);
  }, []);
  
  return <div>Scroll position: {scrollY}px</div>;
};

// Conditional effect execution
const ConditionalEffect = ({ shouldFetch, userId }) => {
  useEffect(() => {
    // Early return to skip effect
    if (!shouldFetch) return;
    
    fetchUser(userId).then(setUser);
  }, [shouldFetch, userId]);
  
  return null;
};

Effect Best Practices Checklist