useEffect: Side Effects in React

Marvin Roque - Feb 18 - - Dev Community

After watching myself struggle with useEffect, let me break down what's actually happening under the hood.

The Lifecycles You Need to Know

Think of your component like a house:

  • Mounting = Building the house (component first appears)
  • Rendering = Redecorating (component updates)
  • Unmounting = Demolishing (component disappears)

Here's how useEffect works with each:

function App() {
  useEffect(() => {
    console.log('🏗️ House built! (Mounted)');

    return () => {
      console.log('🏚️ House demolished! (Unmounted)');
    };
  }, []); // Empty array = only on mount/unmount

  useEffect(() => {
    console.log('🎨 Redecorating! (Re-rendered)');
  }); // No array = every render

  return <div>Hello!</div>;
}
Enter fullscreen mode Exit fullscreen mode

When Effects Actually Run

Here's what happens in real life:

function RoomLight({ isOn }) {
  // 1. Runs after mount AND when isOn changes
  useEffect(() => {
    console.log(`Light turned ${isOn ? 'on' : 'off'}`);
  }, [isOn]);

  // 2. Runs after EVERY render
  useEffect(() => {
    console.log('Room redecorated');
  }); // No dependency array

  // 3. Runs ONCE after mount
  useEffect(() => {
    console.log('Room built');
  }, []); // Empty dependency array

  return <div>Room</div>;
}
Enter fullscreen mode Exit fullscreen mode

Cleanup (The Important Part)

Here's when you need cleanup:

  • Timers
  • Subscriptions
  • Event listeners
  • WebSocket connections
function Timer() {
  useEffect(() => {
    // Set up
    const timer = setInterval(() => {
      console.log('Tick');
    }, 1000);

    // Clean up
    return () => {
      clearInterval(timer); // Prevents memory leaks
    };
  }, []); // Only on mount
}

function WindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
    // Set up
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener('resize', handleResize);

    // Clean up
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []); // Only on mount
}
Enter fullscreen mode Exit fullscreen mode

Common Gotchas I Hit

1. Missing Dependencies

function Counter({ start }) {
  const [count, setCount] = useState(start);

  // Wrong - doesn't update when start changes
  useEffect(() => {
    setCount(start);
  }, []); // Lint will warn you

  // Right - updates when start changes
  useEffect(() => {
    setCount(start);
  }, [start]);
}
Enter fullscreen mode Exit fullscreen mode

2. Running Too Often

function Profile({ user }) {
  // Bad - new object every render
  useEffect(() => {
    console.log('Profile updated');
  }, [{ name: user.name }]); // Runs every time!

  // Good - only when name changes
  useEffect(() => {
    console.log('Profile updated');
  }, [user.name]);
}
Enter fullscreen mode Exit fullscreen mode

Quick Tips

  • The cleanup function runs before the effect runs again
  • Dependencies should include everything that changes
  • Empty array = mount/unmount only
  • No array = every render
  • When in doubt, let the linter guide you

Mental Model

Think of it this way:

useEffect(() => {
  // This runs AFTER render
  console.log('Effect happened');

  return () => {
    // This runs BEFORE next effect or unmount
    console.log('Cleanup happened');
  };
}, [/* dependencies change = effect runs again */]);
Enter fullscreen mode Exit fullscreen mode

That's useEffect stripped down to what matters. Still confused about something? Drop a comment - I love debugging these things.

Follow for more React tips from the trenches 🎯

. . . . . . . . . .