B / React blog
You Probably Don't Need That useEffect
Five common React patterns where useEffect is unnecessary and how to refactor each one into simpler, more predictable code.
- React patterns
- useEffect
- State management
- Refactoring
useEffect is one of the most frequently misunderstood React APIs.
It often becomes the default solution whenever a component needs to do something after rendering: calculate a value, update another piece of state, respond to a button click, reset a form, or keep two state variables synchronized. The problem is that many of these situations do not require an Effect at all.
Using useEffect unnecessarily can introduce extra renders, temporary inconsistent UI, more complicated data flow, dependency-array bugs, infinite loops, state that can become stale, and code that is harder to understand and test.
When the logic only involves React props, state, rendering, or user events, you can usually solve it without an Effect.
This guide covers five common situations where useEffect is unnecessary and shows how to refactor each one.
B-01
1. Derived State
Derived state is a value that can be calculated from existing props or state. Imagine a shopping cart that stores both its items and its total price.
Before: Calculating derived state in an Effect
import { useEffect, useState } from "react";
type CartItem = {
id: string;
name: string;
price: number;
quantity: number;
};
type CartProps = {
items: CartItem[];
};
export function Cart({ items }: CartProps) {
const [total, setTotal] = useState(0);
useEffect(() => {
const nextTotal = items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
setTotal(nextTotal);
}, [items]);
return <p>Total: ${total.toFixed(2)}</p>;
}This works, but the component renders twice when items changes. React first renders with the old total, then the Effect runs and updates total, then React renders again with the correct value. The component also stores duplicated information: total is not independent state, it is entirely determined by items.
After: Calculate the value during rendering
type CartItem = {
id: string;
name: string;
price: number;
quantity: number;
};
type CartProps = {
items: CartItem[];
};
export function Cart({ items }: CartProps) {
const total = items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
return <p>Total: ${total.toFixed(2)}</p>;
}Now there is no extra state, no Effect, no second render, and no risk of items and total becoming inconsistent. The total is always correct because it is calculated from the current items during rendering.
What about expensive calculations?
If the calculation is genuinely expensive, memoize the calculation rather than synchronizing it through state.
import { useMemo } from "react";
export function Cart({ items }: CartProps) {
const total = useMemo(() => {
return items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
}, [items]);
return <p>Total: ${total.toFixed(2)}</p>;
}Do not reach for useMemo automatically. Most array transformations are fast enough to run during rendering. Use memoization only when the calculation has a measurable performance cost.
Refactoring rule
If a value can be calculated from current props or state, calculate it during rendering. Do not store it in state.
B-02
2. Transforming Props
Another common pattern is receiving data through props, transforming it, and saving the result in state. Consider a product list that needs to display only available products.
Before: Copying transformed props into state
import { useEffect, useState } from "react";
type Product = {
id: string;
name: string;
price: number;
inStock: boolean;
};
type ProductListProps = {
products: Product[];
};
export function ProductList({ products }: ProductListProps) {
const [availableProducts, setAvailableProducts] = useState<Product[]>([]);
useEffect(() => {
setAvailableProducts(
products.filter((product) => product.inStock)
);
}, [products]);
return (
<ul>
{availableProducts.map((product) => (
<li key={product.id}>
{product.name} — ${product.price}
</li>
))}
</ul>
);
}availableProducts is not independent state. It is a transformed version of products. This creates two sources of truth that must be manually kept synchronized.
After: Transform props during rendering
export function ProductList({ products }: ProductListProps) {
const availableProducts = products.filter(
(product) => product.inStock
);
return (
<ul>
{availableProducts.map((product) => (
<li key={product.id}>
{product.name} — ${product.price}
</li>
))}
</ul>
);
}The data flow is now direct: products becomes filter, then rendered list. There is no synchronization step in the middle.
Filtering and sorting together
The same principle applies when multiple transformations are required.
Before
const [visibleProducts, setVisibleProducts] = useState<Product[]>([]);
useEffect(() => {
const result = products
.filter((product) =>
product.name.toLowerCase().includes(search.toLowerCase())
)
.sort((a, b) => a.price - b.price);
setVisibleProducts(result);
}, [products, search]);After
const visibleProducts = products
.filter((product) =>
product.name.toLowerCase().includes(search.toLowerCase())
)
.sort((a, b) => a.price - b.price);Be careful with .sort(): it mutates the array it is called on. Since .filter() returns a new array, sorting its result is safe. When sorting props directly, copy the array first: const sortedProducts = [...products].sort((a, b) => a.price - b.price).
For expensive transformations, memoize the result with useMemo.
Refactoring rule
Do not copy props into state just to filter, sort, map, format, or group them. Transform props during rendering.
B-03
3. Event-Driven Logic
Effects run because a component rendered. Event handlers run because a specific user interaction occurred. That distinction matters.
Suppose you want to display a notification after a user adds a product to their cart.
Before: Reacting to event state through an Effect
import { useEffect, useState } from "react";
export function ProductActions() {
const [addedProduct, setAddedProduct] = useState<string | null>(null);
useEffect(() => {
if (!addedProduct) {
return;
}
console.log(`${addedProduct} was added to the cart`);
}, [addedProduct]);
function handleAddToCart(productName: string) {
setAddedProduct(productName);
}
return (
<button onClick={() => handleAddToCart("Mechanical Keyboard")}>
Add to cart
</button>
);
}The code creates state only to trigger an Effect. But the application already knows exactly when the product was added: inside handleAddToCart.
After: Keep event logic in the event handler
export function ProductActions() {
function handleAddToCart(productName: string) {
console.log(`${productName} was added to the cart`);
}
return (
<button onClick={() => handleAddToCart("Mechanical Keyboard")}>
Add to cart
</button>
);
}The relationship between the action and its consequence is now obvious.
A more realistic example
Imagine submitting an order and showing a toast.
Before
const [orderSubmitted, setOrderSubmitted] = useState(false);
useEffect(() => {
if (orderSubmitted) {
toast.success("Your order was submitted");
}
}, [orderSubmitted]);
async function handleSubmit() {
await submitOrder();
setOrderSubmitted(true);
}After
async function handleSubmit() {
await submitOrder();
toast.success("Your order was submitted");
}The notification belongs to the submission event. Using an Effect here creates unnecessary indirection: user submits, then state changes, then component renders, then Effect notices the state, then notification appears. The direct version is simpler: user submits, then order is submitted, then notification appears.
Avoid moving every function into an Effect
A common misconception is that side effects such as API calls must always happen inside useEffect. That is not true. An API call caused by rendering may belong in an Effect or a data-fetching framework. An API call caused by a user action belongs in the event handler.
async function handleDeleteAccount() {
const confirmed = window.confirm(
"Are you sure you want to delete your account?"
);
if (!confirmed) {
return;
}
await deleteAccount();
navigate("/goodbye");
}The action is event-driven, so it does not need an Effect.
Refactoring rule
Ask why the logic should run. If it should run because the user clicked, submitted, selected, dragged, or typed something, put it in the relevant event handler. Do not represent an event as state and then watch that state with an Effect.
B-04
4. Resetting State
Resetting state when a prop changes is a common reason developers add Effects. Consider a comment form that should clear whenever the selected article changes.
Before: Resetting state in an Effect
import { useEffect, useState } from "react";
type CommentFormProps = {
articleId: string;
};
export function CommentForm({ articleId }: CommentFormProps) {
const [comment, setComment] = useState("");
useEffect(() => {
setComment("");
}, [articleId]);
return (
<textarea
value={comment}
onChange={(event) => setComment(event.target.value)}
/>
);
}When articleId changes, React first renders the old component state for the new article. Then the Effect runs and clears the comment. That creates an unnecessary render and briefly associates the old comment with the new article.
After: Reset the component using a key
type CommentSectionProps = {
articleId: string;
};
export function CommentSection({ articleId }: CommentSectionProps) {
return (
<CommentForm
key={articleId}
articleId={articleId}
/>
);
}
function CommentForm({ articleId }: CommentFormProps) {
const [comment, setComment] = useState("");
return (
<form>
<label htmlFor={`comment-${articleId}`}>Comment</label>
<textarea
id={`comment-${articleId}`}
value={comment}
onChange={(event) => setComment(event.target.value)}
/>
</form>
);
}A different key tells React that this is a different component instance. When articleId changes, React removes the previous form and creates a fresh one with fresh state. This is usually the cleanest way to reset all state inside a subtree.
Resetting only part of the state
Sometimes you do not want to reset the entire component. Imagine a product page where the selected image should reset when the product changes, but the zoom preference should remain. You could move the product-specific state into a keyed child.
type ProductPageProps = {
product: Product;
};
export function ProductPage({ product }: ProductPageProps) {
const [zoomEnabled, setZoomEnabled] = useState(false);
return (
<>
<label>
<input
type="checkbox"
checked={zoomEnabled}
onChange={(event) => setZoomEnabled(event.target.checked)}
/>
Enable zoom
</label>
<ProductGallery
key={product.id}
product={product}
zoomEnabled={zoomEnabled}
/>
</>
);
}Now ProductGallery resets when the product changes, but zoomEnabled remains because it belongs to the parent. The component structure communicates which state belongs to which identity.
Another option: Store stable identifiers
Sometimes developers store a selected object and then reset it when the collection changes.
Before
const [selectedProduct, setSelectedProduct] =
useState<Product | null>(null);
useEffect(() => {
setSelectedProduct(null);
}, [products]);A better design may be to store the selected ID instead of the selected object.
const [selectedProductId, setSelectedProductId] =
useState<string | null>(null);
const selectedProduct =
products.find((product) => product.id === selectedProductId) ?? null;Now, if the selected product disappears from the collection, selectedProduct naturally becomes null. There is no reset Effect to maintain.
Refactoring rule
When state should reset because the identity of something changed, consider giving the component a different key, moving the state into a keyed child, or storing a stable identifier instead of duplicating an entire object.
B-05
5. Synchronizing React State with React State
One of the biggest warning signs in a React component is an Effect whose only purpose is updating one state variable when another state variable changes.
Before: Synchronizing two pieces of state
import { useEffect, useState } from "react";
export function RegistrationForm() {
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const [fullName, setFullName] = useState("");
useEffect(() => {
setFullName(`${firstName} ${lastName}`.trim());
}, [firstName, lastName]);
return (
<form>
<input
value={firstName}
onChange={(event) => setFirstName(event.target.value)}
placeholder="First name"
/>
<input
value={lastName}
onChange={(event) => setLastName(event.target.value)}
placeholder="Last name"
/>
<p>Welcome, {fullName}</p>
</form>
);
}fullName is synchronized with firstName and lastName, but it does not need to be state.
After: Calculate the synchronized value
export function RegistrationForm() {
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const fullName = `${firstName} ${lastName}`.trim();
return (
<form>
<input
value={firstName}
onChange={(event) => setFirstName(event.target.value)}
placeholder="First name"
/>
<input
value={lastName}
onChange={(event) => setLastName(event.target.value)}
placeholder="Last name"
/>
<p>Welcome, {fullName}</p>
</form>
);
}There is now a single source of truth.
Keeping filters synchronized
Suppose a dashboard has a selected country and selected city. When the country changes, the city should be cleared.
Before: Watching one state variable to update another
const [country, setCountry] = useState("");
const [city, setCity] = useState("");
useEffect(() => {
setCity("");
}, [country]);This is better handled where the country changes.
After: Update related state in the same event
const [country, setCountry] = useState("");
const [city, setCity] = useState("");
function handleCountryChange(nextCountry: string) {
setCountry(nextCountry);
setCity("");
}Both updates belong to the same user interaction, so keeping them together makes the behavior explicit.
Consider combining related state
When multiple state variables always change together, a reducer or a single state object may better express the relationship.
type LocationState = {
country: string;
city: string;
};
const [location, setLocation] = useState<LocationState>({
country: "",
city: "",
});
function handleCountryChange(country: string) {
setLocation({
country,
city: "",
});
}
function handleCityChange(city: string) {
setLocation((current) => ({
...current,
city,
}));
}For more complex transitions, use a reducer.
type LocationState = {
country: string;
city: string;
};
type LocationAction =
| { type: "countryChanged"; country: string }
| { type: "cityChanged"; city: string };
function locationReducer(
state: LocationState,
action: LocationAction
): LocationState {
switch (action.type) {
case "countryChanged":
return {
country: action.country,
city: "",
};
case "cityChanged":
return {
...state,
city: action.city,
};
default:
return state;
}
}This makes valid state transitions explicit instead of relying on Effects to repair state after rendering.
Refactoring rule
Do not use Effects to make React state catch up with other React state. Instead, derive the value during rendering, update related state in the same event, combine state that always changes together, or use a reducer for complex transitions.
B-06
Why Unnecessary Effects Cause Problems
Unnecessary Effects are not merely a style preference. They can create real bugs.
1. They create extra render cycles
An Effect cannot update state until after React renders. The sequence becomes: render with stale value, then Effect runs, then state updates, then render again. When the value could have been calculated during rendering, the first render was unnecessary.
2. They create temporary inconsistent states
Suppose a product changes, but the selected image is reset inside an Effect. For one render, the component may combine the new product with the old selected image. Even if the user never notices the intermediate state, your component still has to handle it correctly.
3. They duplicate sources of truth
If fullName is state and is also determined by firstName and lastName, which one is authoritative? Every duplicated value introduces a synchronization responsibility.
4. They hide causality
This code is direct: handleSave calls saveDocument and then showSuccessMessage. This code makes the relationship harder to follow: handleSave sets shouldSave to true, and a separate Effect watches shouldSave to call saveDocument and showSuccessMessage. The first version tells you what happens when the user saves. The second requires tracing state changes across different parts of the component.
5. They make dependency management harder
Once logic is inside an Effect, you must manage every reactive dependency it reads. Missing a dependency can produce stale values. Adding a dependency can make the Effect run more often than expected. Trying to silence the linter often hides the underlying design problem instead of solving it.
B-07
When You Actually Need useEffect
The conclusion is not that useEffect is bad. It is essential when a component must synchronize with something outside React.
Browser APIs
useEffect(() => {
document.title = `${unreadCount} unread messages`;
}, [unreadCount]);Event subscriptions
useEffect(() => {
function handleOnline() {
setIsOnline(true);
}
function handleOffline() {
setIsOnline(false);
}
window.addEventListener("online", handleOnline);
window.addEventListener("offline", handleOffline);
return () => {
window.removeEventListener("online", handleOnline);
window.removeEventListener("offline", handleOffline);
};
}, []);Third-party widgets
useEffect(() => {
const map = createMapWidget(containerRef.current);
return () => {
map.destroy();
};
}, []);Network or realtime connections
useEffect(() => {
const connection = createChatConnection(roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]);These Effects synchronize the component with systems whose lifecycle exists outside React. That is what Effects are designed for.
For server data, consider using the data-loading mechanism provided by your framework or a dedicated server-state library. Fetching directly in Effects can be valid, but it requires handling cancellation, race conditions, caching, loading states, and duplicate requests carefully.
B-08
A Practical Refactoring Checklist
Before writing an Effect, ask these questions.
Can I calculate this value during rendering?
If yes, remove the state and calculate the value directly: const fullName = `${firstName} ${lastName}`.
Am I transforming props?
Filter, map, sort, group, or format them during rendering: const visibleItems = items.filter(matchesSearch).
Did a specific user action cause this logic?
Put the logic in the event handler.
function handleSubmit() {
submitForm();
showSuccessToast();
}Should state reset when an entity changes?
Use a key, move the state into a keyed child, or store an identifier.
<Editor key={documentId} documentId={documentId} />Am I updating one state variable because another changed?
Derive the value, update both in the same event, or redesign the state model.
function handleCountryChange(country: string) {
setCountry(country);
setCity("");
}Am I synchronizing with something outside React?
This is where an Effect is likely appropriate. Examples include browser APIs, WebSocket connections, DOM integrations, timers, analytics, third-party libraries, and external subscriptions.
B-09
The Mental Model That Changes Everything
Do not ask how to make this Effect work. Ask why this logic needs to happen.
There are three common answers.
- Because the component rendered: calculate the value during rendering.
- Because the user did something: run the logic in an event handler.
- Because an external system must stay synchronized: use an Effect.
That distinction eliminates a surprising number of Effects.
B-10
Final Takeaway
useEffect should not be the glue holding your React state together. When React state needs to be synchronized with other React state, the component's data model is often the real problem.
Prefer calculating instead of synchronizing, event handlers instead of event-watching Effects, keys instead of manual resets, single sources of truth instead of duplicated state, and reducers instead of state-repair logic.
Effects are an escape hatch for interacting with systems outside React. The fewer unnecessary Effects your components contain, the more predictable your application becomes.
Before adding your next useEffect, pause and ask: is React synchronizing with an external system, or am I using an Effect to compensate for state I did not need?