The React pattern that eliminated 80% of my useEffect calls
← Back
April 2, 2026React6 min read

The React pattern that eliminated 80% of my useEffect calls

Published April 2, 20266 min read

I was auditing a React component that had grown to 340 lines and six useEffect calls. Every time I tried to add a feature it broke something else. When I sat down to understand why, I realised that four of those six effects were doing the same thing: computing a derived value and putting it into state. The fix was not a better effect — it was no effect at all.

The pattern that looks right but is not

Here is the scenario. You have a list of items from an API call. You want to display a filtered, sorted version based on a search term the user typed. The intuitive implementation:

tsx
function ProductList() {
  const [products, setProducts] = useState([]);
  const [searchTerm, setSearchTerm] = useState('');
  const [filteredProducts, setFilteredProducts] = useState([]);

  // Fetch products
  useEffect(() => {
    fetchProducts().then(setProducts);
  }, []);

  // Keep filteredProducts in sync with products + searchTerm
  useEffect(() => {
    const filtered = products.filter(p =>
      p.name.toLowerCase().includes(searchTerm.toLowerCase())
    );
    setFilteredProducts(filtered);
  }, [products, searchTerm]);

  return (
    <ul>
      {filteredProducts.map(p => <li key={p.id}>{p.name}</li>)}
    </ul>
  );
}

This works. But it has a serious flaw: there is a render cycle where products has updated but filteredProducts has not. React renders the component with the stale filteredProducts, then the effect runs, then React renders again. You get a flash of incorrect content and an extra render for free.

More importantly, this pattern adds a state variable that does not need to exist. filteredProducts is not independent state — it is 100% derivable from products and searchTerm. If you can compute something from existing state, it should not be state.

The fix: derived values, not derived state

tsx
function ProductList() {
  const [products, setProducts] = useState([]);
  const [searchTerm, setSearchTerm] = useState('');

  // No useEffect, no extra state — computed inline
  const filteredProducts = products.filter(p =>
    p.name.toLowerCase().includes(searchTerm.toLowerCase())
  );

  useEffect(() => {
    fetchProducts().then(setProducts);
  }, []);

  return (
    <ul>
      {filteredProducts.map(p => <li key={p.id}>{p.name}</li>)}
    </ul>
  );
}

One effect gone. No flash. No extra state. filteredProducts is always consistent with its inputs because it is computed from them on every render.

When the computation is expensive: useMemo

For cheap derivations (filtering a short list, string concatenation), inline computation is fine. For expensive ones — sorting 10,000 items, running a complex algorithm — memoize it:

tsx
import { useMemo } from 'react';

function ProductList() {
  const [products, setProducts] = useState([]);
  const [searchTerm, setSearchTerm] = useState('');
  const [sortKey, setSortKey] = useState<'name' | 'price'>('name');

  const displayProducts = useMemo(() => {
    return products
      .filter(p => p.name.toLowerCase().includes(searchTerm.toLowerCase()))
      .sort((a, b) => a[sortKey] > b[sortKey] ? 1 : -1);
  }, [products, searchTerm, sortKey]);

  // ...
}

The computation only reruns when products, searchTerm, or sortKey change. No effect, no extra state, no extra renders.

The four shapes of bad useEffect

After auditing a dozen components, I found four patterns that almost always indicated a bad effect:

1. Effect that syncs one state from another state

tsx
// Bad
useEffect(() => {
  setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);

// Good
const fullName = `${firstName} ${lastName}`;

2. Effect that transforms props into state

tsx
// Bad — props.items changes, but state is now out of sync until next effect run
function List({ items }: { items: Item[] }) {
  const [sortedItems, setSortedItems] = useState(items);
  useEffect(() => {
    setSortedItems([...items].sort((a, b) => a.name.localeCompare(b.name)));
  }, [items]);
}

// Good
function List({ items }: { items: Item[] }) {
  const sortedItems = useMemo(
    () => [...items].sort((a, b) => a.name.localeCompare(b.name)),
    [items]
  );
}

3. Effect that filters/groups/aggregates existing data

tsx
// Bad
const [totals, setTotals] = useState({ subtotal: 0, tax: 0, total: 0 });
useEffect(() => {
  const subtotal = items.reduce((sum, item) => sum + item.price, 0);
  const tax = subtotal * 0.1;
  setTotals({ subtotal, tax, total: subtotal + tax });
}, [items]);

// Good
const subtotal = items.reduce((sum, item) => sum + item.price, 0);
const tax = subtotal * 0.1;
const total = subtotal + tax;

4. Effect that resets state when a prop changes

tsx
// Bad — often produces a render with stale state
function Pagination({ pageSize }: { pageSize: number }) {
  const [page, setPage] = useState(1);
  useEffect(() => {
    setPage(1);
  }, [pageSize]);
}

// Good — use key to reset component entirely
function Parent() {
  return <Pagination key={pageSize} pageSize={pageSize} />;
}

Which effects are legitimate?

Effects exist for synchronising with systems outside React: fetching data, subscribing to events, setting up timers, manipulating the DOM. If an effect does not involve the outside world, it probably should not be an effect.

A useful mental check before writing an effect: "Is this synchronising with something external, or am I just computing something from my current state?" If it is the latter, reach for a derived value or useMemo first.

The compound benefit

Fewer effects means fewer dependencies to audit, fewer stale closure bugs, fewer "why is this rendering twice" debugging sessions, and fewer opportunities for state to get out of sync with itself.

When I refactored that 340-line component, removing four derived-state effects dropped it to 220 lines. More importantly, it became predictable — every render computes its display values fresh from a single source of truth, rather than chasing state updates through a chain of effects.

The React team has a great section on this in the updated docs: "You don't need Effects for events" and "Calculating state based on other state". Worth reading once you have seen the pattern a few times — it reframes how you think about the component model entirely.

Share this
← All Posts6 min read