Forem

Marvin Roque
Marvin Roque

Posted on

useEffect: Side Effects in React

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 🎯

Top comments (0)