Frontend Development 15 min read

Avoiding Unnecessary useEffect Usage in React: Best Practices and Patterns

This article explains the pitfalls of overusing the useEffect hook in React, demonstrates how to replace redundant effects with direct state calculations, memoization, and event‑handler logic, and provides practical code examples for improving performance and simplifying component design.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Avoiding Unnecessary useEffect Usage in React: Best Practices and Patterns

The ideas in this article are inspired by the React documentation "You Might Not Need an Effect" and present the author’s understanding after multiple readings, aiming to share practical best‑practice tips for React developers.

useEffect pain points : The callback of useEffect runs as an asynchronous macro‑task after the view is updated, causing extra renders when the effect modifies state, which can lead to performance loss and visual flicker.

How to remove unnecessary effects :

Derive data needed for rendering directly from props or state inside the function component instead of placing the conversion in an effect. Use useMemo for expensive calculations.

Handle user‑driven data changes in event handlers rather than in effects.

function Counter() {
  const [count, setCount] = useState(0);
  function handleClick() {
    setCount(prev => prev + 1);
  }
  // avoid putting the logic in useEffect
}

Derived state similar to Vue computed properties :

function Form() {
  const [firstName, setFirstName] = useState('Taylor');
  const [lastName, setLastName] = useState('Swift');
  // ❌ unnecessary state and effect
  const [fullName, setFullName] = useState('');
  useEffect(() => {
    setFullName(firstName + ' ' + lastName);
  }, [firstName, lastName]);
  // ✅ compute directly during render
  const fullName = firstName + ' ' + lastName;
}

Caching expensive calculations with useMemo :

import { useMemo } from 'react';
function TodoList({ todos, filter }) {
  const visibleTodos = useMemo(() => getFilteredTodos(todos, filter), [todos, filter]);
}

Resetting all state when props change (e.g., a profile page that should clear comment input when userId changes):

export default function ProfilePage({ userId }) {
  const [comment, setComment] = useState('');
  // ❌ resetting state inside an effect
  useEffect(() => {
    setComment('');
  }, [userId]);
  // ✅ use a changing key to force a fresh mount
  return
;
}

Adjusting part of the state when a prop changes (e.g., synchronising selection with a list of items):

function List({ items }) {
  const [isReverse, setIsReverse] = useState(false);
  const [selectedId, setSelectedId] = useState(null);
  const selection = items.find(item => item.id === selectedId) ?? null;
}

Sharing logic in event handlers instead of chaining effects (e.g., adding a product to cart and showing a notification):

function ProductPage({ product, addToCart }) {
  function buyProduct() {
    addToCart(product);
    showNotification(`已添加 ${product.name} 进购物车!`);
  }
  function handleBuyClick() { buyProduct(); }
  function handleCheckoutClick() { buyProduct(); navigateTo('/checkout'); }
}

When an effect is truly needed (e.g., fetching analytics on first render):

function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');
  // ✅ run once on mount
  useEffect(() => {
    post('/analytics/event', { eventName: 'visit_form' });
  }, []);
  function handleSubmit(e) {
    e.preventDefault();
    post('/api/register', { firstName, lastName });
  }
}

Avoiding chained effects that cause multiple re‑renders (game example):

function Game() {
  const [card, setCard] = useState(null);
  const [goldCardCount, setGoldCardCount] = useState(0);
  const [round, setRound] = useState(1);
  const [isGameOver, setIsGameOver] = useState(false);
  // ❌ multiple effects just to trigger each other
  useEffect(() => { if (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(() => { if (isGameOver) alert('游戏结束!'); }, [isGameOver]);
  // ✅ compute everything in the event handler
  function handlePlaceCard(nextCard) {
    if (round > 5) throw Error('游戏已经结束了。');
    setCard(nextCard);
    if (nextCard.gold) {
      if (goldCardCount <= 3) setGoldCardCount(goldCardCount + 1);
      else { setGoldCardCount(0); setRound(round + 1); if (round === 5) alert('游戏结束!'); }
    }
  }
}

App initialization in React Strict Mode : because effects with an empty dependency array run twice, guard the logic with a module‑level flag.

let didInit = false;
function App() {
  useEffect(() => {
    if (!didInit) {
      didInit = true;
      loadDataFromLocalStorage();
      checkAuthToken();
    }
  }, []);
}

Notifying parent components about state changes should be done directly in the event callback, not via an effect.

function Toggle({ onChange }) {
  const [isOn, setIsOn] = useState(false);
  function updateToggle(next) {
    setIsOn(next);
    onChange(next);
  }
  function handleClick() { updateToggle(!isOn); }
  function handleDragEnd(e) {
    if (isCloserToRightEdge(e)) updateToggle(true);
    else updateToggle(false);
  }
}

Passing data to a parent should be done through props from the parent, not by the child effect.

function Parent() {
  const data = useSomeAPI();
  return
;
}
function Child({ data }) {
  // use data directly, no effect needed
}

Subscribing to external stores – use the built‑in useSyncExternalStore hook instead of manually adding listeners inside an effect.

function subscribe(callback) {
  window.addEventListener('online', callback);
  window.addEventListener('offline', callback);
  return () => {
    window.removeEventListener('online', callback);
    window.removeEventListener('offline', callback);
  };
}
function useOnlineStatus() {
  return useSyncExternalStore(
    subscribe,
    () => navigator.onLine,
    () => true
  );
}
function ChatIndicator() {
  const isOnline = useOnlineStatus();
  // render based on isOnline
}

Fetching asynchronous data inside an effect should include a cleanup function to ignore stale responses and avoid race conditions.

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; };
  }, [query, page]);
  function handleNextPageClick() { setPage(page + 1); }
}
frontendperformanceReactBest PracticesHooksuseEffect
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.