Home / Articles

Picking the Right React Tool: Derive, Handle, Fetch, Defer or Effect

A decision guide for replacing reflexive useEffect calls with derived values, event handlers, a data layer, useTransition, measured useMemo and React 19 APIs.

Publisher: REACTAPP.TOP8 min read1896 words

This article is published in English.

Sources: Stop Using `useEffect` for Everything: 7 React Patterns That Make Your Code Simpler

Many React components collect useEffect calls for fetching, reacting to props, computing values and keeping state aligned. A few months later you open the file and find four Effects, three pieces of derived state, two loading flags and a bug nobody can explain. This guide gives you a single question to ask before writing any hook, "does this actually need an Effect?", and seven concrete patterns for what to use when the answer is no.

Derived values belong in render, not in state

The most frequent misuse is storing something that can be computed. Suppose you show products filtered by a search query. A common first attempt adds a state variable for the result:

const [filteredProducts, setFilteredProducts] = useState([]);

and then an Effect that recomputes it whenever the inputs change:

useEffect(() => {
  setFilteredProducts(
    products.filter(product =>
      product.name.toLowerCase().includes(query.toLowerCase())
    )
  );
}, [products, query]);

This works, but filteredProducts is not independent state. It is a pure function of values you already hold:

products
+
query
=
filteredProducts

Storing it means React first renders stale results, then runs the Effect and renders again. Compute it directly in the component body instead:

const filteredProducts = products.filter(product =>
  product.name.toLowerCase().includes(query.toLowerCase())
);

You end up with no extra state variable, no Effect, nothing to keep in sync, and no second render just to produce the result. If the filtering is genuinely costly, for instance over a very large list, wrap the same expression in useMemo:

const filteredProducts = useMemo(() => {
  return products.filter(product =>
    product.name.toLowerCase().includes(query.toLowerCase())
  );
}, [products, query]);

The memo is a detail. What matters is the principle: a value derived from props or state does not need a home in state.

User actions belong in event handlers

Another pattern routes a click through state so that an Effect can react to it. First a flag:

const [shouldSave, setShouldSave] = useState(false);

then an Effect that watches the flag, plus a handler that sets it:

useEffect(() => {
  if (shouldSave) {
    saveUser();
    setShouldSave(false);
  }
}, [shouldSave]);function handleSave() {
  setShouldSave(true);
}

There is no reason for the detour. The click is the moment the action should happen, and you already have a function that runs at that moment:

function handleSave() {
  saveUser();
}

Compare the two flows. The flag version goes through several steps before anything is saved, and then triggers yet another render to reset itself:

Click
  ↓
State update
  ↓
Render
  ↓
Effect
  ↓
saveUser()
  ↓
Another state update
  ↓
Render

The direct version has one step:

Click
  ↓
saveUser()

The rule of thumb is that events are handled in event handlers, and Effects exist to synchronize with systems outside React, not to disguise ordinary event logic.

Copy props into state only when you need two values

This one looks harmless. A component keeps its own copy of a name:

const [name, setName] = useState('');

and an Effect overwrites that copy whenever the prop changes:

useEffect(() => {
  setName(user.name);
}, [user]);

Now there are two sources of truth, and at any given moment you have to ask which one is correct:

user.name
     +
name

If all you do is display the value, read it straight from the prop:

return <h2>{user.name}</h2>;

There is a legitimate exception: an edit form that deliberately holds a temporary, editable copy of server data until the user saves or cancels:

Server data
    ↓
Edit form
    ↓
User changes fields
    ↓
Save / Cancel

Here local state is correct because the draft really is a separate value with its own lifecycle. The lesson is not "never copy props into state" but "know why you are creating a second source of truth." For the form case, resetting the draft by giving the form a key tied to the record ID is often cleaner than syncing with an Effect; our article on replacing state-syncing Effects walks through that technique in detail.

Let a data layer own fetching

Data loading tends to spread across Effects. It starts with one request on mount:

useEffect(() => {
  fetchUser();
}, []);

and grows into a row of Effects that each fetch something for the same user:

useEffect(() => {
  fetchOrders();
}, [userId]);useEffect(() => {
  fetchNotifications();
}, [userId]);useEffect(() => {
  fetchPermissions();
}, [userId]);

At that point the component has become an API orchestration layer. In larger apps this is where a dedicated data-fetching solution pays off. Depending on your stack, that could be:

  • TanStack Query
  • SWR
  • the data-fetching features of your framework
  • Server Components, where they fit

Otherwise you are hand-rolling every one of these concerns for each request:

loading
error
data
refetch
cache
retry
stale data
race conditions

A data layer handles all of that for you. Conceptually, a query declares what data it needs and how to get it, and returns the current status:

const { data, isLoading, error } = useQuery({
  queryKey: ['user', userId],
  queryFn: () => fetchUser(userId)
});

The query key doubles as a cache key, so components asking for the same user share one request, and changing userId fetches the new user without stale responses winning. The component goes back to rendering UI instead of acting as a small networking library.

Mark non-urgent updates with useTransition

Consider a search box that drives an expensive table. With a single state update, every keystroke forces the table to re-render before the input can show the next character:

function handleChange(e) {
  setQuery(e.target.value);
}

The fix is to split the value in two: what the input displays, and what the table filters by:

const [inputValue, setInputValue] = useState('');
const [query, setQuery] = useState('');

The input value is updated normally, while the query update is wrapped in startTransition:

const [isPending, startTransition] = useTransition();function handleChange(e) {
  const value = e.target.value;  setInputValue(value);  startTransition(() => {
    setQuery(value);
  });
}

React now treats the two updates differently. Typing is urgent:

Typing
↓
Urgent

while the filtering and re-rendering can be interrupted and delayed:

Filtering/rendering
↓
Can wait

A transition does not make slow code faster; it changes scheduling so that the input stays responsive and a newer keystroke can interrupt an in-progress render. isPending lets you hint that the table is catching up.

Measure before you memoize

The opposite failure is optimization by reflex. It often begins with one memoized calculation:

const result = useMemo(() => {
  return calculateSomething(data);
}, [data]);

followed by a memoized mapping:

const users = useMemo(() => {
  return data.map(...);
}, [data]);

then a memoized column definition:

const columns = useMemo(() => [...], []);

and memoized options:

const options = useMemo(() => [...], []);

Soon every value in the component is wrapped, yet nobody ever checked whether anything was slow. useMemo helps with expensive work and stable references for memoized children, but optimization should follow a loop:

Problem
  ↓
Measure
  ↓
Find bottleneck
  ↓
Optimize
  ↓
Measure again

and not a shortcut:

Component feels complicated
  ↓
Add useMemo

For cheap calculations, memoization only adds reading overhead and dependency arrays that can go stale. The React DevTools Profiler tells you whether a component is really the bottleneck.

Use the primitives React 19 added

Several habits from earlier versions of React now have simpler replacements. For years, passing a ref into your own component meant wrapping it in forwardRef. In React 19, function components receive ref as an ordinary prop:

function CustomInput({ ref, ...props }) {
  return <input ref={ref} {...props} />;
}

Optimistic UI is another example. Instead of inserting a temporary item into state by hand and remembering to remove or replace it later:

setTodos(prev => [
  ...prev,
  {
    id: 'temp',
    text,
    pending: true
  }
]);

you can use useOptimistic, which derives a temporary view from the real state:

const [optimisticTodos, addOptimisticTodo] = useOptimistic(
  todos,
  (state, todo) => [...state, todo]
);

The optimistic value is shown while an async action is in progress and falls back to the real todos once it settles, so there is no temporary item to clean up. It is meant to be called inside an Action or transition; the edge cases around failed requests are covered in our look at useOptimistic rollback failure modes.

For IDs that link labels to inputs for accessibility, use the built-in hook:

const id = useId();

rather than generating one at random:

const id = `input-${Math.random()}`;

A random ID changes on every render and differs between server and client, breaking hydration and the label association. React keeps adding primitives that express intent directly, so avoid solving current problems with patterns that predate them.

A decision checklist before you add a hook

Knowing React well includes knowing when to leave hooks out. A component is not better for using all of these:

useEffect()
useMemo()
useCallback()
useRef()
useState()

Aim for a data flow that is easy to follow. Before reaching for a hook, name the problem and match it to the tool.

For derived data:

Calculate it.

For a user action:

Handle the event.

For synchronizing with something outside React, such as a subscription, a timer or a non-React widget:

Consider useEffect.

For reading from an external store:

Consider useSyncExternalStore or a state library.

For an update that matters but is not urgent:

Consider useTransition.

For a calculation that might be expensive:

Measure first, then consider useMemo.

And when React already ships a primitive for the job:

Use the primitive instead of rebuilding it yourself.

Key takeaways

  • The healthiest components tend to have the fewest unnecessary hooks, not the most.
  • Replace "where can I use useEffect?" with "does this actually need an Effect?" Most of the time the answer points to render-time calculation, an event handler or a data library.
  • Learn the problem each new React API solves before its syntax.
  • Sometimes the most effective optimization in a component is deleting a hook rather than adding one.