State is a Record of the Past, Not a Claim About the Present
One of the most common React mistakes I see is using useEffect to keep two pieces of state in sync. It seems intuitive, but it introduces a subtle bug — and more importantly, it reveals a misunderstanding about what state actually represents.
The Problem
Imagine you have a list of items and a selected item ID. When items change, you need to clear the selection if the selected item no longer exists.
Here’s the instinctive (but wrong) approach:
function ItemList() {
const [selectedId, setSelectedId] = useState<string | null>(null)
const items = useItems()
useEffect(() => {
if (selectedId !== null && !items.some((item) => item.id === selectedId)) {
setSelectedId(null)
}
}, [items, selectedId])
return <List items={items} selectedId={selectedId} />
}
This looks reasonable, but there’s a hidden problem: for one render, selectedId points to an item that doesn’t exist.
React renders with stale state, then the effect runs and fixes it. During that intermediate render, any code that assumes selectedId is valid will break.
But there’s a deeper issue here — this code misunderstands what state is for.
The Mental Shift
When you call setSelectedId("abc"), you’re not declaring “item abc is selected.” You’re recording an event: “the user selected item abc.”
State is a record of what happened. It’s historical. It doesn’t make claims about whether that selection is still valid — that’s a separate concern.
This reframing changes everything. Watch what happens when we rename the variable to reflect what it actually holds:
function ItemList() {
const [lastSelectedId, setLastSelectedId] = useState<string | null>(null)
const items = useItems()
const selectedId = useMemo(
() => items.some((item) => item.id === lastSelectedId) ? lastSelectedId : null,
[items, lastSelectedId],
)
return <List items={items} selectedId={selectedId} />
}
Now the code reads honestly:
lastSelectedId— the last ID the user selected (a fact about the past)selectedId— the currently valid selection (derived from present reality)
There’s no useEffect. No sync. No intermediate broken render. The derivation happens synchronously during render, so selectedId is always consistent with items.
The Principle
State records what happened. Derived values describe what’s true now.
When you try to store “what’s true now” directly in state, you end up fighting to keep it updated — hence the useEffect trap. But when you store the raw historical fact and derive current truth from it, everything stays in sync automatically.
A Reusable Hook
This pattern comes up often enough that it’s worth extracting:
function useSelection<TItem, TKey extends keyof TItem>(
items: TItem[],
identifierKey: TKey,
): [TItem[TKey] | null, (id: TItem[TKey] | null) => void] {
const [lastSelectedId, setLastSelectedId] = useState<TItem[TKey] | null>(null)
const selectedId = useMemo(
() => items.some((item) => item[identifierKey] === lastSelectedId) ? lastSelectedId : null,
[items, identifierKey, lastSelectedId],
)
return [selectedId, setLastSelectedId]
}
Usage is clean:
function ItemList() {
const items = useItems()
const [selectedId, setSelectedId] = useSelection(items, 'id')
return <List items={items} selectedId={selectedId} />
}
The hook encapsulates the pattern: store the user’s action, derive the valid state. Consumers get a selectedId that’s always safe to use.
Takeaway
Next time you reach for useEffect to “fix” state based on other state, pause. Ask yourself: am I storing a fact about the past, or am I trying to store current truth?
Store the past. Derive the present.

