You might not need an Effect
Spot the Effects you don't need, and replace them with derived values, event handlers and keys.
Effects are an escape hatch. Once you learn them, it is tempting to use them for everything: "when X changes, set Y". Most of those Effects make your app slower, longer and buggier.
This lesson is a beginner-sized tour of one of the most useful pages in the React docs, You Might Not Need an Effect. Its core rule fits on one line: if there is no external system involved, you probably don't need an Effect.
Derived state: calculate it during render
Here is an Effect that keeps a full name in sync with a first and last name:
// Avoid: redundant state and an unnecessary Effect
const [firstName, setFirstName] = useState('Ada');
const [lastName, setLastName] = useState('Lovelace');
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);It works, but it is wasteful. React renders with the old fullName, runs the Effect, sets state, then renders again with the new value. For a moment the screen even shows stale data. You already know the fix from Thinking in React: if you can calculate it, don't store it.
// Better: calculated during render
const [firstName, setFirstName] = useState('Ada');
const [lastName, setLastName] = useState('Lovelace');
const fullName = firstName + ' ' + lastName;The same goes for filtered lists, totals, counts and anything else derived from props or state:
What about slow calculations?
Most calculations like these take well under a millisecond, even for hundreds of items. If you measure one and it really is slow, wrap it in useMemo. It remembers the result and only recalculates when its dependencies change:
import { useMemo } from 'react';
const visibleTodos = useMemo(() => getFilteredTodos(todos, filter), [todos, filter]);useMemo is a performance tool, not a correctness tool, so don't sprinkle it everywhere. On Day 6 you will turn on the React Compiler, which adds this kind of memoization for you automatically.
A component receives items as a prop and needs to show how many are selected. What is the best approach?
Event handler or Effect?
Sometimes an Effect isn't calculating anything; it is doing something in response to a change. The question to ask is: why does this code run?
- If it runs because the user did something (clicked Buy, submitted a form), it belongs in that event handler.
- If it runs because the component was displayed and needs to stay in sync (connect to a chat, log a page view), it belongs in an Effect.
Here is a classic mistake. A form sets a flag, and an Effect watches the flag to send the data:
// Avoid: event-specific logic inside an Effect
const [submitted, setSubmitted] = useState(false);
useEffect(() => {
if (submitted) {
post('/api/register', { firstName, lastName });
}
}, [submitted]);
function handleSubmit(e) {
e.preventDefault();
setSubmitted(true);
}The request happens because the user pressed submit. That is an event, so do it in the handler. No flag, no Effect:
// Better: the event handler does the work directly
function handleSubmit(e) {
e.preventDefault();
post('/api/register', { firstName, lastName });
}Moving logic into handlers also avoids surprise re-runs. In the Effect version, if submitted were ever reset and set again by unrelated code, or if the component re-mounted with submitted already true, the request would fire again.
A common anti-pattern is useEffect(() => onChange(isOn), [isOn]). It runs one render late, fires on mount even though nothing changed, and makes the parent re-render twice. Calling onChange inside the handler means React updates both components together in one pass.
Resetting state when a prop changes: use a key
A profile page shows a comment box. When you navigate to a different user, the half-typed comment should clear. You might write:
// Avoid: resetting state in an Effect
function ProfilePage({ userId }) {
const [comment, setComment] = useState('');
useEffect(() => {
setComment('');
}, [userId]);
// ...
}This renders once with the stale comment, then again after the Effect clears it. And every piece of state inside the page (and its children) would need its own reset line. Instead, use what you learned about state and position: give the component a key tied to the user. A different key means React treats it as a different component and starts its state fresh.
Type a note and move the slider, then switch users. Both reset at once, with no Effect at all. Try deleting key={userId} to see the difference.
When you DO need an Effect
Effects aren't bad. They are the right tool when you need to sync with something outside React:
- connecting to a server, a WebSocket or a chat room;
- subscribing to browser events such as
resizeoronline; - controlling a non-React widget, such as a map or a video player;
- saving to
localStorage(you will do this in the custom hooks lesson); - fetching data that should match what is on screen (next lesson).
Which of these is a good use of an Effect?
Remove the unnecessary Effects
This component works, but it has two Effects it doesn't need. Remove both without changing what the user sees.
Show solution
results is derived from query, so calculate it during render. The message appears because the user clicked Add, so set it in the click handler. The addedTitle state is no longer needed at all.
import { useState } from 'react';
const allBooks = [
{ id: '1', title: 'The Hobbit' },
{ id: '2', title: 'Dune' },
{ id: '3', title: 'Piranesi' },
{ id: '4', title: 'Project Hail Mary' },
];
export default function App() {
const [query, setQuery] = useState('');
const [message, setMessage] = useState('');
const results = allBooks.filter((b) => b.title.toLowerCase().includes(query.toLowerCase()));
function handleAdd(title) {
setMessage('Added ' + title + ' to your list!');
}
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Filter" />
<ul>
{results.map((b) => (
<li key={b.id}>
{b.title} <button onClick={() => handleAdd(b.title)}>Add</button>
</li>
))}
</ul>
<p>{message}</p>
</>
);
}As a bonus, the handler version also shows the message again if you add the same book twice. The Effect version didn't, because addedTitle didn't change.
Day 4 progress