DEV Community

Cover image for You Might Not Need an Effect - Study Guide
Thoithoi Shougrakpam
Thoithoi Shougrakpam

Posted on

You Might Not Need an Effect - Study Guide

Source: https://react.dev/learn/you-might-not-need-an-effect

Core Principle

Effects are an escape hatch from the React paradigm. They let you "step outside" of React and synchronize your components with external systems like:

  • Non-React widgets
  • Network
  • Browser DOM

If there is no external system involved (for example, if you want to update a component's state when some props or state change), you shouldn't need an Effect.

Removing unnecessary Effects will make your code:

  • βœ… Easier to follow
  • βœ… Faster to run
  • βœ… Less error-prone

When You DON'T Need Effects

1. Transforming Data for Rendering

❌ Bad: Using Effect to update state

function Form() {
  const [firstName, setFirstName] = useState('Taylor');
  const [lastName, setLastName] = useState('Swift');

  // πŸ”΄ Avoid: redundant state and unnecessary Effect
  const [fullName, setFullName] = useState('');
  useEffect(() => {
    setFullName(firstName + ' ' + lastName);
  }, [firstName, lastName]);
}
Enter fullscreen mode Exit fullscreen mode

Problem: This does an entire render pass with a stale value, then immediately re-renders with the updated value.

βœ… Good: Calculate during rendering

function Form() {
  const [firstName, setFirstName] = useState('Taylor');
  const [lastName, setLastName] = useState('Swift');
  // βœ… Good: calculated during rendering
  const fullName = firstName + ' ' + lastName;
}
Enter fullscreen mode Exit fullscreen mode

When something can be calculated from existing props or state, don't put it in state. Calculate it during rendering instead.


2. Caching Expensive Calculations

❌ Bad: Using Effect with state

function TodoList({ todos, filter }) {
  const [newTodo, setNewTodo] = useState('');

  // πŸ”΄ Avoid: redundant state and unnecessary Effect
  const [visibleTodos, setVisibleTodos] = useState([]);
  useEffect(() => {
    setVisibleTodos(getFilteredTodos(todos, filter));
  }, [todos, filter]);
}
Enter fullscreen mode Exit fullscreen mode

βœ… Good: Use useMemo for expensive calculations

function TodoList({ todos, filter }) {
  const [newTodo, setNewTodo] = useState('');

  // βœ… Does not re-run unless todos or filter change
  const visibleTodos = useMemo(() => {
    return getFilteredTodos(todos, filter);
  }, [todos, filter]);
}
Enter fullscreen mode Exit fullscreen mode

Or even simpler if the calculation is not expensive:

const visibleTodos = getFilteredTodos(todos, filter);
Enter fullscreen mode Exit fullscreen mode

The function you wrap in useMemo runs during rendering, so this only works for pure calculations.


3. Resetting All State When a Prop Changes

❌ Bad: Using Effect to reset state

export default function ProfilePage({ userId }) {
  const [comment, setComment] = useState('');

  // πŸ”΄ Avoid: Resetting state on prop change in an Effect
  useEffect(() => {
    setComment('');
  }, [userId]);
}
Enter fullscreen mode Exit fullscreen mode

Problem: This is inefficient because ProfilePage and its children will first render with the stale value, and then render again.

βœ… Good: Use a key to reset state

export default function ProfilePage({ userId }) {
  return (
    <Profile
      userId={userId}
      key={userId} // βœ… This and any other state below will reset on key change automatically
    />
  );
}

function Profile({ userId }) {
  const [comment, setComment] = useState('');
  // ...
}
Enter fullscreen mode Exit fullscreen mode

By passing userId as a key, you're asking React to treat two Profile components with different userId as two different components that should not share any state.


4. Adjusting Some State When a Prop Changes

❌ Bad: Using Effect to sync derived state

function List({ items }) {
  const [isReverse, setIsReverse] = useState(false);
  const [selection, setSelection] = useState(null);

  // πŸ”΄ Avoid: Adjusting state on prop change in an Effect
  useEffect(() => {
    setSelection(null);
  }, [items]);
}
Enter fullscreen mode Exit fullscreen mode

βœ… Better: Adjust state during rendering

function List({ items }) {
  const [isReverse, setIsReverse] = useState(false);
  const [selection, setSelection] = useState(null);

  // βœ… Best: Calculate everything during rendering
  const [prevItems, setPrevItems] = useState(items);
  if (items !== prevItems) {
    setPrevItems(items);
    setSelection(null);
  }
}
Enter fullscreen mode Exit fullscreen mode

βœ… Best: Calculate everything during rendering

function List({ items }) {
  const [isReverse, setIsReverse] = useState(false);
  const [selectedId, setSelectedId] = useState(null);

  // βœ… Best: Calculate everything during rendering
  const selection = items.find(item => item.id === selectedId) ?? null;
}
Enter fullscreen mode Exit fullscreen mode

Storing information from previous renders can be hard to understand, but it's better than updating the same state in an Effect. Always check whether you can reset all state with a key or calculate everything during rendering instead.


5. Sharing Logic Between Event Handlers

❌ Bad: Using Effect for event-specific logic

function ProductPage({ product, addToCart }) {
  // πŸ”΄ Avoid: Event-specific logic inside an Effect
  useEffect(() => {
    if (product.isInCart) {
      showNotification(`Added ${product.name} to the shopping cart!`);
    }
  }, [product]);

  function handleBuyClick() {
    addToCart(product);
  }

  function handleCheckoutClick() {
    addToCart(product);
    navigateTo('/checkout');
  }
}
Enter fullscreen mode Exit fullscreen mode

Problem: This Effect is unnecessary and will cause bugs. The notification will appear every time the page reloads if the product is already in the cart.

βœ… Good: Extract shared logic into a function

function ProductPage({ product, addToCart }) {
  // βœ… Good: Event-specific logic is called from event handlers
  function buyProduct() {
    addToCart(product);
    showNotification(`Added ${product.name} to the shopping cart!`);
  }

  function handleBuyClick() {
    buyProduct();
  }

  function handleCheckoutClick() {
    buyProduct();
    navigateTo('/checkout');
  }
}
Enter fullscreen mode Exit fullscreen mode

Key Question: Ask yourself whether some code should be in an Effect or in an event handler:

  • Event Handler: If this logic is caused by a particular interaction
  • Effect: If it's caused by the user seeing the component on the screen

6. Sending POST Requests

❌ Bad: Form submission in Effect

function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');

  // πŸ”΄ Avoid: Event-specific logic inside an Effect
  const [jsonToSubmit, setJsonToSubmit] = useState(null);
  useEffect(() => {
    if (jsonToSubmit !== null) {
      post('/api/register', jsonToSubmit);
    }
  }, [jsonToSubmit]);

  function handleSubmit(e) {
    e.preventDefault();
    setJsonToSubmit({ firstName, lastName });
  }
}
Enter fullscreen mode Exit fullscreen mode

βœ… Good: Analytics in Effect, form submission in event handler

function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');

  // βœ… Good: This logic should run because the component was displayed
  useEffect(() => {
    post('/analytics/event', { eventName: 'visit_form' });
  }, []);

  function handleSubmit(e) {
    e.preventDefault();
    // βœ… Good: Event-specific logic is in the event handler
    post('/api/register', { firstName, lastName });
  }
}
Enter fullscreen mode Exit fullscreen mode

Use Effects only for code that should run because the component was displayed to the user.


7. Chains of Computations

❌ Bad: Chaining Effects

function Game() {
  const [card, setCard] = useState(null);
  const [goldCardCount, setGoldCardCount] = useState(0);
  const [round, setRound] = useState(1);
  const [isGameOver, setIsGameOver] = useState(false);

  // πŸ”΄ Avoid: Chains of Effects that adjust the state solely to trigger each other
  useEffect(() => {
    if (card !== null && card.gold) {
      setGoldCardCount(c => c + 1);
    }
  }, [card]);

  useEffect(() => {
    if (goldCardCount > 3) {
      setRound(r => r + 1);
      setGoldCardCount(0);
    }
  }, [goldCardCount]);

  useEffect(() => {
    if (round > 5) {
      setIsGameOver(true);
    }
  }, [round]);

  useEffect(() => {
    alert('Good game!');
  }, [isGameOver]);
}
Enter fullscreen mode Exit fullscreen mode

Problems:

  1. Very inefficient: component re-renders between each set call
  2. Rigid and fragile: hard to evolve as requirements change

βœ… Good: Calculate during rendering, update in event handler

function Game() {
  const [card, setCard] = useState(null);
  const [goldCardCount, setGoldCardCount] = useState(0);
  const [round, setRound] = useState(1);

  // βœ… Calculate what you can during rendering
  const isGameOver = round > 5;

  function handlePlaceCard(nextCard) {
    if (isGameOver) {
      throw Error('Game already ended.');
    }

    // βœ… Calculate all the next state in the event handler
    setCard(nextCard);
    if (nextCard.gold) {
      if (goldCardCount <= 3) {
        setGoldCardCount(goldCardCount + 1);
      } else {
        setGoldCardCount(0);
        setRound(round + 1);
        if (round === 5) {
          alert('Good game!');
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

This is a lot more efficient. Also, if you implement a way to view game history, you can set each state variable to a move from the past without triggering the Effect chain.


8. Initializing the Application

❌ Bad: App initialization in Effect

function App() {
  // πŸ”΄ Avoid: Effects with logic that should only ever run once
  useEffect(() => {
    loadDataFromLocalStorage();
    checkAuthToken();
  }, []);
}
Enter fullscreen mode Exit fullscreen mode

Problem: This will run twice in development, which can cause issues (e.g., invalidating authentication token).

βœ… Good: Top-level initialization

if (typeof window !== 'undefined') { // Check if we're running in the browser
  checkAuthToken();
  loadDataFromLocalStorage();
}

function App() {
  // ...
}
Enter fullscreen mode Exit fullscreen mode

βœ… Alternative: Use a flag

let didInit = false;

function App() {
  useEffect(() => {
    if (!didInit) {
      didInit = true;
      // βœ… Only runs once per app load
      loadDataFromLocalStorage();
      checkAuthToken();
    }
  }, []);
}
Enter fullscreen mode Exit fullscreen mode

Code at the top level runs once when your component is importedβ€”even if it doesn't end up being rendered.


9. Notifying Parent Components About State Changes

❌ Bad: Using Effect to call parent's onChange

function Toggle({ onChange }) {
  const [isOn, setIsOn] = useState(false);

  // πŸ”΄ Avoid: The onChange handler runs too late
  useEffect(() => {
    onChange(isOn);
  }, [isOn, onChange]);

  function handleClick() {
    setIsOn(!isOn);
  }
}
Enter fullscreen mode Exit fullscreen mode

Problem: The Toggle updates its state first, and React updates the screen. Then React runs the Effect, which calls the onChange function passed from a parent component. Now the parent component will update its own state, starting another render pass.

βœ… Good: Update both in the same event handler

function Toggle({ onChange }) {
  const [isOn, setIsOn] = useState(false);

  function updateToggle(nextIsOn) {
    // βœ… Good: Perform all updates during the event that caused them
    setIsOn(nextIsOn);
    onChange(nextIsOn);
  }

  function handleClick() {
    updateToggle(!isOn);
  }

  function handleDragEnd(e) {
    if (isCloserToRightEdge(e)) {
      updateToggle(true);
    } else {
      updateToggle(false);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

With this approach, both the Toggle component and its parent component update their state during the event. React batches updates from different components together, so there will only be one render pass.

βœ… Even Better: Lift state up (fully controlled component)

// βœ… Also good: the component is fully controlled by its parent
function Toggle({ isOn, onChange }) {
  function handleClick() {
    onChange(!isOn);
  }

  function handleDragEnd(e) {
    if (isCloserToRightEdge(e)) {
      onChange(true);
    } else {
      onChange(false);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

"Lifting state up" lets the parent component fully control the Toggle by toggling the parent's own state.


10. Passing Data to the Parent

❌ Bad: Fetching in child and passing to parent

function Parent() {
  const [data, setData] = useState(null);
  // ...
  return <Child onFetched={setData} />;
}

function Child({ onFetched }) {
  const data = useSomeAPI();

  // πŸ”΄ Avoid: Passing data to the parent in an Effect
  useEffect(() => {
    if (data) {
      onFetched(data);
    }
  }, [onFetched, data]);
}
Enter fullscreen mode Exit fullscreen mode

Problem: In React, data flows from the parent components to their children. When you see something wrong on the screen, you can trace where the information comes from by going up the component chain.

βœ… Good: Fetch in parent and pass down

function Parent() {
  const data = useSomeAPI();
  // ...
  // βœ… Good: Passing data down to the child
  return <Child data={data} />;
}

function Child({ data }) {
  // ...
}
Enter fullscreen mode Exit fullscreen mode

This is simpler and keeps the data flow predictable: the data flows down from the parent to the child.


When You DO Need Effects

1. Subscribing to External Stores

⚠️ Manual subscription (not ideal)

function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(true);

  useEffect(() => {
    function updateState() {
      setIsOnline(navigator.onLine);
    }

    updateState();

    window.addEventListener('online', updateState);
    window.addEventListener('offline', updateState);

    return () => {
      window.removeEventListener('online', updateState);
      window.removeEventListener('offline', updateState);
    };
  }, []);

  return isOnline;
}
Enter fullscreen mode Exit fullscreen mode

βœ… Better: Use useSyncExternalStore

function subscribe(callback) {
  window.addEventListener('online', callback);
  window.addEventListener('offline', callback);
  return () => {
    window.removeEventListener('online', callback);
    window.removeEventListener('offline', callback);
  };
}

function useOnlineStatus() {
  // βœ… Good: Subscribing to an external store with a built-in Hook
  return useSyncExternalStore(
    subscribe, // React won't resubscribe for as long as you pass the same function
    () => navigator.onLine, // How to get the value on the client
    () => true // How to get the value on the server
  );
}
Enter fullscreen mode Exit fullscreen mode

This approach is less error-prone than manually syncing mutable data to React state with an Effect.


2. Fetching Data

βœ… Fetching in Effect (with proper cleanup)

function SearchResults({ query }) {
  const [results, setResults] = useState([]);
  const [page, setPage] = useState(1);

  useEffect(() => {
    let ignore = false;

    fetchResults(query, page).then(json => {
      if (!ignore) {
        setResults(json);
      }
    });

    return () => {
      ignore = true; // βœ… Cleanup function to ignore stale responses
    };
  }, [query, page]);

  function handleNextPageClick() {
    setPage(page + 1);
  }
}
Enter fullscreen mode Exit fullscreen mode

Why this is appropriate:

  • You want to keep results synchronized with the current query and page
  • The fetch happens because the component was displayed, not because of a specific interaction

Important: The cleanup function with ignore flag prevents race conditions when typing fast.

βœ… Even Better: Use a framework or custom hook

function SearchResults({ query }) {
  const [page, setPage] = useState(1);
  const params = new URLSearchParams({ query, page });

  // βœ… Modern frameworks provide more efficient built-in data fetching
  const results = useData(`/api/search?${params}`);

  function handleNextPageClick() {
    setPage(page + 1);
  }
}
Enter fullscreen mode Exit fullscreen mode

Modern frameworks provide more efficient built-in data fetching mechanisms that handle:

  • Caching responses
  • Avoiding network waterfalls
  • Prefetching data
  • Server-side rendering

Key Takeaways

Decision Tree: Effect vs Event Handler

Ask yourself: What kind of logic is this from the user's perspective?

  • Caused by a particular interaction β†’ Event Handler
  • Caused by the user seeing the component β†’ Effect

State Behaves Like a Snapshot

function handleClick() {
  setRound(round + 1); // round is still the old value here

  // If you need the next value:
  const nextRound = round + 1;
  // Use nextRound for calculations
}
Enter fullscreen mode Exit fullscreen mode

Inside event handlers, state reflects the value at the time the user clicked.

Benefits of Avoiding Unnecessary Effects

  1. Faster - Avoids extra render passes and cascading updates
  2. Simpler - Less code, easier to understand data flow
  3. Less error-prone - Avoids bugs from synchronization issues
  4. More maintainable - Easier to modify and extend

When Effects ARE Appropriate

  • Synchronizing with external systems (browser APIs, third-party libraries, network)
  • Data fetching (with proper cleanup for race conditions)
  • Subscribing to external stores (prefer useSyncExternalStore)

Common Patterns Summary

Scenario ❌ Don't Use Effect βœ… Use Instead
Transform data Effect + setState Calculate during render
Cache expensive calc Effect + setState useMemo
Reset all state Effect on prop change key prop
Adjust some state Effect on prop change Calculate during render
Share event logic Effect Extracted function
POST on user action Effect Event handler
POST on page view Event handler Effect
Chain of updates Multiple Effects Single event handler
App initialization Effect Top-level code
Notify parent Effect calling onChange Call in event handler
Pass data to parent Effect Lift state up
Subscribe to store Manual Effect useSyncExternalStore
Fetch data Manual Effect Framework/custom hook

Remember: Effects are a powerful feature, but they're also an escape hatch. Use them when you need to synchronize with systems outside of React, not for orchestrating your application's data flow.

Top comments (0)