Overview
"You might not need an effect" is a React design principle: use effects for synchronization with external systems, not for ordinary data flow inside React. An external system might be a browser API, a network connection, a timer, a subscription, a non-React widget, analytics, or imperative DOM integration.
Many effects in React applications are unnecessary because they are trying to do work that can happen during rendering or in an event handler. Removing those effects usually makes code simpler, faster, and less bug-prone.
Bad pattern:
function Form({ firstName, lastName }: Props) {
const [fullName, setFullName] = useState("");
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
return <p>{fullName}</p>;
}
Better:
function Form({ firstName, lastName }: Props) {
const fullName = `${firstName} ${lastName}`;
return <p>{fullName}</p>;
}
For interviews, this topic matters because it shows whether a developer understands React's rendering model. A strong candidate should be able to explain when to derive values during render, when to handle logic directly in event handlers, when to reset state with a key, when to store IDs instead of duplicated objects, and when an effect is genuinely appropriate.
The practical goal is to use effects as escape hatches, not as a default way to coordinate React state.
Core Concepts
Effects Are for External Synchronization
Use an effect when a component needs to synchronize with something outside React.
Good effect example:
function ChatRoom({ roomId }: { roomId: string }) {
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]);
return <h1>Room {roomId}</h1>;
}
This is appropriate because a network connection exists outside React. The effect starts synchronization, and the cleanup stops it.
Common external systems:
- Browser events.
- Timers.
- WebSocket or SignalR connections.
- Third-party widgets.
- Imperative APIs.
- Subscriptions.
- Analytics that should run because a screen appeared.
- Client-side data fetching when no framework or data library owns it.
If there is no external system, pause before writing an effect.
Pattern: Calculate Derived Values During Render
If a value can be calculated from props or state, calculate it during render.
Bad:
function InvoiceSummary({ items }: { items: InvoiceItem[] }) {
const [total, setTotal] = useState(0);
useEffect(() => {
setTotal(items.reduce((sum, item) => sum + item.price, 0));
}, [items]);
return <p>Total: {total}</p>;
}
Better:
function InvoiceSummary({ items }: { items: InvoiceItem[] }) {
const total = items.reduce((sum, item) => sum + item.price, 0);
return <p>Total: {total}</p>;
}
The effect version renders once with stale total, then renders again after the effect sets state. The render-time calculation is simpler and avoids an extra render pass.
Good derived values:
fullNamefromfirstNameandlastName.totalfrom cart items.isValidfrom form fields.visibleItemsfrom items and a filter.selectedItemfromitemsandselectedId.
Pattern: Use useMemo for Expensive Pure Calculations
Most derived calculations should just run during render. If a pure calculation is noticeably expensive and its inputs often stay the same, cache it with useMemo.
function TodoList({
todos,
filter,
}: {
todos: Todo[];
filter: string;
}) {
const visibleTodos = useMemo(
() => getFilteredTodos(todos, filter),
[todos, filter]
);
return <List items={visibleTodos} />;
}
Use useMemo for performance, not correctness. The component should still behave correctly if the calculation runs every render.
Avoid this:
const [visibleTodos, setVisibleTodos] = useState<Todo[]>([]);
useEffect(() => {
setVisibleTodos(getFilteredTodos(todos, filter));
}, [todos, filter]);
That creates redundant state and an unnecessary effect.
Pattern: Put Event-Specific Logic in Event Handlers
If logic happens because the user did something, put it in the event handler.
Bad:
function ProductPage({ product }: { product: Product }) {
const [isInCart, setIsInCart] = useState(false);
useEffect(() => {
if (isInCart) {
showNotification(`${product.name} added to cart`);
}
}, [isInCart, product.name]);
return (
<button onClick={() => setIsInCart(true)}>
Add to cart
</button>
);
}
Better:
function ProductPage({ product }: { product: Product }) {
function handleAddToCart() {
addToCart(product.id);
showNotification(`${product.name} added to cart`);
}
return (
<button onClick={handleAddToCart}>
Add to cart
</button>
);
}
The notification should happen because the user clicked the button, not because the component later observed a state value.
Event-handler logic includes:
- Submitting forms.
- Showing user-action notifications.
- Navigating after a click.
- Starting a checkout flow.
- Updating state in response to input.
- Calling a parent callback.
Pattern: Reset State with a Key
If changing a prop means a subtree should be treated as a different instance, use a key.
Bad:
function ProfilePage({ userId }: { userId: string }) {
const [comment, setComment] = useState("");
useEffect(() => {
setComment("");
}, [userId]);
return <CommentBox value={comment} onChange={setComment} />;
}
This renders once with the old comment, then clears it after the effect runs.
Better:
function ProfilePage({ userId }: { userId: string }) {
return <Profile key={userId} userId={userId} />;
}
function Profile({ userId }: { userId: string }) {
const [comment, setComment] = useState("");
return <CommentBox value={comment} onChange={setComment} />;
}
Changing the key tells React that this is a different profile instance, so local state below it resets naturally.
Use this when the identity of a screen, form, or subtree truly changes.
Pattern: Store IDs Instead of Duplicated Objects
If you store both a collection and a selected object from that collection, the selected object can become stale.
Bad:
const [items, setItems] = useState(initialItems);
const [selectedItem, setSelectedItem] = useState(initialItems[0]);
Better:
const [items, setItems] = useState(initialItems);
const [selectedId, setSelectedId] = useState(initialItems[0]?.id ?? null);
const selectedItem =
items.find((item) => item.id === selectedId) ?? null;
Now item data has one source of truth, and selection is derived from the current list.
This often removes effects like:
useEffect(() => {
setSelectedItem(null);
}, [items]);
Instead of adjusting duplicated state, store a stable identity and derive the object.
Pattern: Avoid Chains of Effects
A chain of effects is when one effect updates state, which triggers another effect, which updates more state.
Bad:
useEffect(() => {
if (card !== null) {
setGoldCardCount((count) => count + 1);
}
}, [card]);
useEffect(() => {
if (goldCardCount > 3) {
setRound(round + 1);
}
}, [goldCardCount, round]);
This makes the flow indirect and can create extra renders. Prefer calculating the next values in the event handler or reducer that knows what happened.
Better:
function handleCardDrawn(card: Card) {
setState((state) => {
const nextGoldCount =
card.kind === "gold" ? state.goldCardCount + 1 : state.goldCardCount;
return {
...state,
card,
goldCardCount: nextGoldCount,
round: nextGoldCount > 3 ? state.round + 1 : state.round,
};
});
}
When state transitions are related, keep them in one event handler or reducer.
Pattern: Notify Parents from the Event That Changed the Value
If a child changes a value and the parent needs to know, call the parent callback from the event handler.
function Toggle({
checked,
onCheckedChange,
}: {
checked: boolean;
onCheckedChange: (checked: boolean) => void;
}) {
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
onCheckedChange(event.currentTarget.checked);
}
return (
<input
type="checkbox"
checked={checked}
onChange={handleChange}
/>
);
}
Avoid this pattern:
useEffect(() => {
onCheckedChange(checked);
}, [checked, onCheckedChange]);
The parent callback should usually be part of the event flow, not a reaction to a later render.
Pattern: Initialize State Correctly
If initial state is expensive, pass an initializer function to useState.
const [todos, setTodos] = useState(() => loadInitialTodos());
Do not initialize with an effect unless the initialization depends on an external system that must happen after render.
Bad:
const [todos, setTodos] = useState<Todo[]>([]);
useEffect(() => {
setTodos(loadInitialTodos());
}, []);
Better:
const [todos, setTodos] = useState(() => loadInitialTodos());
For values based on props, be clear whether the prop is an initial value or a controlled value.
function Editor({ initialText }: { initialText: string }) {
const [text, setText] = useState(initialText);
}
The name initialText communicates that later prop changes are not expected to overwrite local edits.
Pattern: Split Actual Effects by Purpose
Sometimes you do need effects, but one effect is doing multiple unrelated jobs.
Bad:
useEffect(() => {
document.title = title;
const connection = createConnection(roomId);
connection.connect();
return () => connection.disconnect();
}, [title, roomId]);
Better:
useEffect(() => {
document.title = title;
}, [title]);
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]);
Each effect should describe one synchronization process. This makes dependencies and cleanup easier to reason about.
Data Fetching Nuance
Fetching in an effect can be acceptable for simple client-only components:
useEffect(() => {
let ignore = false;
async function load() {
const data = await fetchUser(userId);
if (!ignore) {
setUser(data);
}
}
load();
return () => {
ignore = true;
};
}, [userId]);
However, data fetching is often better handled by framework loaders, server rendering, server components, route-level APIs, or client data libraries that provide caching, deduplication, race handling, and preloading.
Interview answer: effects can fetch data, but manual effect fetching is not automatically the best production data-loading architecture.
Decision Checklist
Before adding an effect, ask:
- Is there an external system involved?
- Can this value be calculated during render?
- Is this logic caused by a user event?
- Can state be reset with a key?
- Can duplicated state be replaced with an ID or derived value?
- Are multiple related state updates better handled in one event handler or reducer?
- Should this be a custom hook because it synchronizes with an external system?
- Is a framework or data library a better home for data fetching?
If the answer does not involve external synchronization, the effect may be unnecessary.
Common Mistakes
Common mistakes include:
- Using effects to calculate
fullName, totals, filtered lists, or validation booleans. - Updating state in an effect immediately after render when the value could be derived.
- Moving event-specific logic into effects.
- Resetting state on prop change with an effect instead of a key.
- Storing duplicated objects and trying to sync them with effects.
- Chaining effects that update each other's dependencies.
- Suppressing the dependency linter instead of changing the code.
- Treating effects as lifecycle methods instead of synchronization processes.
- Fetching data manually in every component without thinking about caching or race conditions.
Best Practices
Use these rules of thumb:
- Effects are escape hatches for external synchronization.
- Derive values during render whenever possible.
- Use event handlers for user-triggered actions.
- Store the minimal source of truth.
- Reset state with keys when component identity changes.
- Prefer IDs and derived lookup over duplicated selected objects.
- Use reducers for related state transitions.
- Split unrelated effects.
- Let dependency warnings guide refactoring instead of suppressing them.