Stop the Double Render: When to Derive State in Render, Reset with Keys, and Keep useEffect for External Systems
Avoid double renders by deriving values during render, resetting state with keys, and reserving useEffect for external systems. Learn when to keep logic in event handlers, when to memoize, and how to measure performance.
25 Sept 2025, 05:34 UTC

Why the Double Render Matters
When a component renders once with stale data, runs useEffect to sync, and then renders again, the UI briefly shows an incorrect state. In development this shows up as a flicker, and in production it can cause race‑condition bugs, especially under React.StrictMode which intentionally double‑invokes effects.
Thesis: Derive, Don’t Duplicate
React’s official guidance says useEffect should only be used to talk to the outside world. If you’re copying props into state, you’re probably doing the same work twice. Keeping that logic in the render phase keeps the component self‑contained and eliminates a whole class of stale‑state bugs.
1. Derived Values During Render
Consider a search filter that shows only items whose name matches a query. The anti‑pattern is to store the filtered list in state and update it in an effect:
function Search({items, query}) {
const [visible, setVisible] = useState([]);
useEffect(() => {
setVisible(items.filter(i => i.name.includes(query)));
}, [items, query]);
return <ul>{visible.map(i => <li key={i.id}>{i.name}</li>)}</ul>
}
Every time query changes, React renders Search, runs the effect, then re‑renders with the new visible array. The first render shows an empty list, a fleeting flicker that can be confusing.
Derive the value directly:
function Search({items, query}) {
const visible = items.filter(i => i.name.includes(query));
return <ul>{visible.map(i => <li key={i.id}>{i.name}</li>)}</ul>
}
Now the component renders once with the correct list. The code is simpler, the dependency graph is explicit, and React.StrictMode no longer double‑renders a stale state.
2. Resetting State with a Key
Sometimes you need to reset a component’s internal state when a prop changes – for example, clearing a form when the selected user changes. The idiomatic solution is to give the component a key that changes with the prop. React will unmount the old instance and mount a fresh one, resetting all state.
function UserForm({user}) {
// internal form state
const [name, setName] = useState(user.name);
/* … */
}
// parent
<UserForm key={user.id} user={user} />
Inside UserForm you no longer need an effect that compares user to previous props. The key guarantees a clean slate, and the component stays free of prop‑to‑state sync logic.
3. Event Handlers vs. useEffect
Logic that responds to user actions belongs in the event handler. If you submit a form, you fire the POST request and any analytics tracking in that same handler. useEffect is reserved for side‑effects that react to rendering itself, such as updating document.title or subscribing to a WebSocket.
Example: a button that toggles a modal and logs the action.
function ModalToggle() {
const [open, setOpen] = useState(false);
const handleClick = () => {
setOpen(o => !o);
analytics.track('modal_toggle', {open: !open});
};
return <button onClick={handleClick}>{open ? 'Close' : 'Open'}</button>;
}
The analytics call is part of the user‑initiated flow, not a reaction to rendering.
4. When useEffect Is Still Needed
External systems that React can’t express as state – browser APIs, third‑party widgets, subscriptions – still require useEffect. For example:
useEffect(() => {
const handleResize = () => setSize(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
Notice the empty dependency array: the effect runs once on mount and cleans up on unmount. This pattern is safe because it doesn’t depend on props or state that change over time.
5. Memoization Trade‑Offs
When a derived value is expensive – large lists, heavy transforms – useMemo can prevent unnecessary recomputation:
const visible = useMemo(() => {
return items.filter(i => i.name.includes(query));
}, [items, query]);
However, premature memoization adds complexity. Measure first: React’s profiler can show whether the render cost is significant. In many cases, the compiler and V8 optimize simple operations, making manual memoization unnecessary.
Limitations & Practical Checks
- Derived values should be cheap; otherwise,
useMemoor a data‑fetching library may be more appropriate. - Key‑based resets work only when the prop change truly warrants a fresh component. Overusing keys can lead to unnecessary unmounts.
- Always verify that
useEffectisn’t inadvertently mutating state that should be derived. - In React 19+, the compiler may automatically memoize certain patterns; check your build tool’s documentation.
Actionable Takeaway
When you find yourself writing an effect that copies props into state, pause and ask: can I compute this value during render? If yes, do it. If the component needs to reset its state on a prop change, wrap it in a key. Keep useEffect for truly external work. Measure expensive computations before adding useMemo – the compiler and runtime may already be doing what you need.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.