Optimizing your React code with best practices for React Hooks
React Hooks are powerful tools for managing state and side effects in React applications. However, the useEffect
hook is often overused—leading to overly complex, error-prone, and inefficient components. In this post, we explore when not to use useEffect
and how to leverage React’s built-in capabilities for cleaner, more efficient code.
useEffect
Unless Absolutely NecessaryuseEffect
to update state variables derivable in render.useEffect
to cache calculations.useEffect
to handle user events.useEffect
only for synchronizing with external systems after a render.Whenever a React component needs to update—either because it received new props or its internal state changed— React will re-run the component function to produce new JSX. This is often referred to as a “re-render.” After the render completes, any registered useEffect
hooks are evaluated. If an effect updates state, React will trigger yet another re-render, and the cycle continues.
This cycle means any state update you perform inside a useEffect
will lead to additional re-renders. Overusing or misusing useEffect
—for instance, setting state in ways that could be computed directly during the render—can create unnecessary render loops, degrade performance, and produce a bad developer experience.
Every time your component renders, React:
useState
and simple constants.useEffect
(executed after the DOM update).When useEffect
is overused or misapplied (especially for computations that can be done within the component’s render), it can:
useEffect
Below are some common, but incorrect, utilizations of useEffect, along with better alternatives.
Rule of Thumb
useEffect
should not be used to update state variables.
The Issue:
Using useEffect
to derive or update state variables (which could simply be computed during the render) will cause a re-render after the effect runs—potentially leading to redundant render cycles.
Example (WRONG):
const Wrong = ({ firstName, lastName }: Props) => {
const [fullName, setFullName] = useState<string>('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
return (
<Container>
<Typography>{fullName}</Typography>
</Container>
);
};
Better Approach (RIGHT):
const Right = ({ firstName, lastName }: Props) => {
// Or directly in JSX
const fullName = `${firstName} ${lastName}`;
return (
<Container>
<Typography>{fullName}</Typography>
</Container>
);
};
useEffect
Rule of Thumb
useEffect should not handle state variable calculations
The Issue:
Calculating derived data in a useEffect
and storing it in state can cause unnecessary re-renders, because every time that state updates, the component re-renders. If the calculations are pure and only depend on existing props or state, you can typically do this calculation during the render—or use a memoization hook like useMemo
. This is quite similar to the previous example but it’s helpful to know that even more complicated computations should not be placed in a useEffect when updating a state variable.
Example (WRONG):
const Wrong = ({ items }: Props) => {
const [filteredItems, setFilteredItems] = useState(items);
const [activeFilter, setActiveFilter] = useState<string | null>(null);
useEffect(() => {
if (activeFilter) {
setFilteredItems(items.filter((item) => item.type === activeFilter));
} else {
setFilteredItems(items);
}
}, [activeFilter, items]);
return (
<Container>
<Box>
<Button onClick={() => setActiveFilter('red')}>Red</Button>
<Button onClick={() => setActiveFilter('blue')}>Blue</Button>
<Button onClick={() => setActiveFilter('green')}>Green</Button>
</Box>
<Box>
{filteredItems.map((item) => (
<Typography key={item.name}>{item.name}</Typography>
))}
</Box>
</Container>
);
};
Better Approach (RIGHT):
const Right = ({ items }: Props) => {
const [activeFilter, setActiveFilter] = useState<string | null>(null);
const filter = (item: { name: string; type: string }) => {
if (activeFilter === null) {
return true;
}
return item.type === activeFilter;
};
// Ideal for expensive calculations
// const filteredItems = useMemo(() => items.filter(filter), [activeFilter]);
const filteredItems = items.filter(filter);
return (
<Container>
<Box>
<Button onClick={() => setActiveFilter('red')}>Red</Button>
<Button onClick={() => setActiveFilter('blue')}>Blue</Button>
<Button onClick={() => setActiveFilter('green')}>Green</Button>
</Box>
<Box>
{filteredItems.map((item) => (
<Typography key={item.name}>{item.name}</Typography>
))}
</Box>
</Container>
);
};
useEffect
Rule of Thumb
useEffect
should not handle user events
The Issue:
Using useEffect
to manage user-driven side effects complicates event handling and can cause errors. Instead, handle those actions directly in an event handler.
For example, in an ecommerce app, if you save an item to cart by adding it to local storage, then the incorrect logic below would show a notification on reload if a user already added an item to cart. We don’t want that message to appear on load, but only at the moment when a user adds the item to cart.
Example (WRONG):
const Wrong = ({ items, selectedItem, setSelectedItem }: Props) => {
const { createSnackNotice } = useSnackbar();
useEffect(() => {
if (selectedItem) {
createSnackNotice(
`User selected ${selectedItem.name}`,
SnackbarVariant.Success
);
}
}, [selectedItem]);
return (
<Container>
<Box>
{items.map((item) => (
<Typography onClick={() => setSelectedItem(item)} key={item.name}>
{item.name}
</Typography>
))}
</Box>
</Container>
);
};
export default Wrong;
Better Approach (RIGHT):
const Right = ({ items, setSelectedItem }: Props) => {
const { createSnackNotice } = useSnackbar();
const handleSelection = (item: { name: string; type: string }) => {
setSelectedItem(item);
createSnackNotice(`User selected ${item.name}`, SnackbarVariant.Success);
};
return (
<Container>
<Box>
{items.map((item) => (
<Typography onClick={() => handleSelection(item)} key={item.name}>
{item.name}
</Typography>
))}
</Box>
</Container>
);
};
useEffect
?Despite the potential pitfalls, useEffect
is still valuable for synchronizing your component with external systems after it has rendered. Use it for:
Rule of Thumb
useEffect should only be used because the component was displayed
“Effects are an escape hatch from the React paradigm. They let you step outside of React and synchronize your components with external systems.” — You Might Not Need an Effect
In many cases, React’s built-in mechanisms and specialized libraries like Apollo Client's useQuery
, React Router’s useSearchParams
, or React’s useSyncExternalStore
can provide cleaner alternatives to useEffect. The useEffect is meant to be a last resort to do things in a “non-React” way. Knowing that frontend programs can be complicated, React gave us an “escape hatch”, but it’s best stay within the paradigm whenever feasible.