Remove Unnecessary useEffect and Derived State in React

Flags useEffect calls that copy props into state, compute values for rendering, or chain state updates that belong in render or an event handler.

What it flags

Flaggedcomponents/ProductResults.tsx
import { useEffect, useState } from 'react';
import { ProductGrid } from './ProductGrid';
import type { Product } from './types';

type Props = { products: Product[]; query: string };

export function ProductResults({ products, query }: Props) {
  const [matches, setMatches] = useState<Product[]>([]);
  const [heading, setHeading] = useState('');

  // Renders once with no matches, then re-renders once per effect
  useEffect(() => {
    const needle = query.toLowerCase();
    setMatches(products.filter((p) => p.name.toLowerCase().includes(needle)));
  }, [products, query]);

  useEffect(() => {
    setHeading(`${matches.length} of ${products.length} products`);
  }, [matches, products]);

  return <ProductGrid heading={heading} products={matches} />;
}
Passescomponents/ProductResults.tsx
import { useMemo } from 'react';
import { ProductGrid } from './ProductGrid';
import type { Product } from './types';

type Props = { products: Product[]; query: string };

export function ProductResults({ products, query }: Props) {
  const matches = useMemo(() => {
    const needle = query.toLowerCase();
    return products.filter((p) => p.name.toLowerCase().includes(needle));
  }, [products, query]);

  const heading = `${matches.length} of ${products.length} products`;

  return <ProductGrid heading={heading} products={matches} />;
}

Why it matters

An effect that sets state from props or other state makes React commit a render with the stale value and then render again with the new one, so users can briefly see outdated UI and every chained effect adds another pass. Logic moved from an event handler into an effect that watches state also runs when that state changes for other reasons, such as a notification firing again when a saved cart loads. These effects look harmless in a diff because the final screen is usually correct.

Agent instructions

cubic checks every pull request against these instructions. Paste them into AGENTS.md or Cursor rules and your AI coding tool follows them while it writes code.

AGENTS.md

Add this section to AGENTS.md at the repository root. Codex, Cursor, and other AI coding tools read it.

## Remove Unnecessary useEffect and Derived State in React

Applies to `**/*.tsx`, `**/*.jsx`, `**/use*.ts`, `**/use*.js`.

Review React components and hooks for effects that duplicate render logic or chain state updates.

Flag changes that:
- Call a state setter inside useEffect with a value computed only from props or other state, such as a filtered list or a formatted label; compute it during render instead, with useMemo if it is expensive.
- Mirror a prop into local state with useEffect(() => setValue(prop), [prop]) instead of reading the prop directly or making the component controlled.
- Reset local state in an effect when an ID prop changes, where rendering the component with key={id} would remount it with fresh state.
- Chain effects where one effect sets state only to trigger another effect, so a single change cascades through several renders.
- Run user-triggered work, such as submitting a form, sending a click analytics event, or showing a toast, in an effect that watches a flag set by an event handler instead of in the handler.
- Notify a parent through an onChange prop inside an effect after local state changes, instead of calling it in the event handler that updates the state.

Do not flag:
- Effects that synchronize with systems outside React, such as subscriptions, timers, DOM measurement, third-party widgets, or network requests.
- State seeded once from a prop on purpose, such as an editable draft initialized from initialValue, when later prop changes should not overwrite it.
- setState calls that the project's ESLint config already reports as errors through react-hooks/set-state-in-effect, part of the eslint-plugin-react-hooks 7 recommended preset.

Checks files matching **/*.tsx, **/*.jsx, **/use*.ts, **/use*.js.

More React agents

See all React code review standards

Run this agent on every pull request

cubic reviews every pull request against the agents you turn on and comments on the lines that break them.

Catch this in your code