# Don't Use useEffect for Render Logic

**NEVER** · **ID:** `performance-ibelick-no-effect-render` · **Category:** performance
**Source:** [@Ibelick](https://www.ui-skills.com/)
**Interactive version:** https://ui-guides-agent-rules.netlify.app/principles/performance-ibelick-no-effect-render

> NEVER: Use useEffect for anything that can be expressed as render logic. Derive state during render, not in effects. Effects are for synchronization with external systems.

Never use useEffect to compute values that should be derived during render

The state-syncing effect — useEffect(() => setFullName(first + " " + last), [first, last]) — is not just slower, it is a second source of truth. React must render with the stale value, commit it, run the effect, set state, and render again: the user briefly sees the old value, and any bug in the dependency array leaves the two permanently out of step. Derived values are just expressions: compute them during render (const fullName = first + " " + last), and reach for useMemo only when the computation is genuinely expensive. Effects are for synchronising with something OUTSIDE React — the DOM, a subscription, the network — not with React's own state.

## Bad — do not do this

`performance-ibelick-no-effect-render-bad`

```tsx
import { useState, useEffect, useRef } from 'react';

export function IbelickNoEffectRenderBad() {
  const [items] = useState(['Apple', 'Banana', 'Cherry', 'Date']);
  const [filter, setFilter] = useState('');
  const [filteredItems, setFilteredItems] = useState<string[]>(items);

  const renders = useRef(0);
  renders.current += 1;

  // Derived state via an effect: render, then setState, then render again.
  useEffect(() => {
    setFilteredItems(items.filter((item) => item.toLowerCase().includes(filter.toLowerCase())));
  }, [items, filter]);

  return (
    <div className="space-y-3">
      <div className="flex items-center justify-between">
        <span className="text-xs text-muted-foreground">Type a letter</span>
        <span className="rounded-md bg-muted px-2 py-1 text-xs font-medium tabular-nums text-destructive">
          renders: {renders.current}
        </span>
      </div>
      <input
        type="text"
        value={filter}
        onChange={(e) => setFilter(e.target.value)}
        placeholder="Filter items..."
        className="w-full px-3 py-2 bg-background border border-border rounded-lg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
      />
      <ul className="space-y-1">
        {filteredItems.map((item) => (
          <li key={item} className="text-sm">
            {item}
          </li>
        ))}
      </ul>
      <p className="text-xs text-destructive">
        This counter climbs twice as fast as the Good one: the input sets state, React renders, then the effect sets
        state again and React renders a second time. (React double-renders in development, so both numbers are
        doubled. The ratio is the point.)
      </p>
    </div>
  );
}
```

## Good — do this

`performance-ibelick-no-effect-render-good`

```tsx
import { useState, useMemo, useRef } from 'react';

export function IbelickNoEffectRenderGood() {
  const [items] = useState(['Apple', 'Banana', 'Cherry', 'Date']);
  const [filter, setFilter] = useState('');

  const renders = useRef(0);
  renders.current += 1;

  // Derived during render: no second pass.
  const filteredItems = useMemo(
    () => items.filter((item) => item.toLowerCase().includes(filter.toLowerCase())),
    [items, filter]
  );

  return (
    <div className="space-y-3">
      <div className="flex items-center justify-between">
        <span className="text-xs text-muted-foreground">Type a letter</span>
        <span className="rounded-md bg-muted px-2 py-1 text-xs font-medium tabular-nums text-success">
          renders: {renders.current}
        </span>
      </div>
      <input
        type="text"
        value={filter}
        onChange={(e) => setFilter(e.target.value)}
        placeholder="Filter items..."
        className="w-full px-3 py-2 bg-background border border-border rounded-lg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
      />
      <ul className="space-y-1">
        {filteredItems.map((item) => (
          <li key={item} className="text-sm">
            {item}
          </li>
        ))}
      </ul>
      <p className="text-xs text-success">
        This counter climbs half as fast. The filtered list is computed during render, so a keystroke costs one render
        pass instead of two
      </p>
    </div>
  );
}
```

## References

- [ibelick — baseline-ui SKILL.md](https://github.com/ibelick/ui-skills/blob/main/skills/baseline-ui/SKILL.md)
- [You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect)
