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 four of those six effects were doing the same thing. They were computing a derived value and dumping it into state.

The fix wasn't a better effect. It was no effect at all.

The pattern that looks right but isn't

You have a list of items from an API. 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. It also has a serious flaw. There's a render cycle where products has updated but filteredProducts hasn't. React renders the component with the stale filteredProducts, then the effect runs, then React renders again. You get a flash of incorrect content and a bonus render.

The deeper problem: this adds a state variable that doesn't need to exist. filteredProducts isn't independent state, it's 100% derivable from products and searchTerm. If you can compute something from existing state, it shouldn't 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's computed from them on every render.

When the computation is expensive: useMemo

For cheap derivations (filtering a short list, string concatenation), inline is fine. For expensive ones, like sorting 10,000 items or running a heavier 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.

Four shapes of bad useEffect

After auditing a dozen components, I kept hitting the same four patterns. Each of them 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 doesn't touch the outside world, it probably shouldn't be an effect.

The mental check I use before writing one: "Is this synchronising with something external, or am I just computing something from my current state?" If it's 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.

When I refactored that 340-line component, removing four derived-state effects dropped it to 220 lines. The bigger win was predictability. Every render now computes its display values fresh from a single source of truth, instead of chasing state updates through a chain of effects.

The React team has a good 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've seen the pattern a few times.

Share this
← All Posts6 min read