A React list that is already on screen flips back to its <Suspense> fallback every time the user changes a filter that triggers a new data read. Why does the revealed content disappear, and how do you keep it visible while the new data loads?
answer
- boundaries react to every suspension, not just the first
- urgent vs non-urgent update
- transitions keep the old screen
- state survives, but hidden is hidden
- isPending drives the inline affordance
basics
~20 sThe filter change is an urgent update, so re-suspending the boundary immediately swaps the revealed list for its fallback. Marking that update as a transition with startTransition or useTransition tells React to keep the old content on screen until the new data is ready.
solid answer
~50 sNothing is broken — that is the default. A `<Suspense>` boundary does not only show its fallback on first load; whenever its subtree suspends again, the boundary goes back to the fallback, so an urgent state change that makes the list read new data hides the list you were looking at. The fix is to tell React the update is not urgent: wrap the state change in `startTransition`, or use `useTransition` and call the setter inside its `startTransition`. During a transition React will not replace already-revealed content with a fallback — it keeps the previous UI committed and swaps in the new one only when it is ready. You then show progress with the `isPending` flag from `useTransition`, typically dimming the list or enabling a small inline spinner, rather than blanking the region. Note that a boundary that has never revealed still shows its fallback; the suppression only protects content already on screen.
code
jsx · 25 linesimport { useState, useTransition, Suspense } from 'react';
import ResultsList from './ResultsList';
export default function Search() {
const [filter, setFilter] = useState('');
const [isPending, startTransition] = useTransition();
function onChange(event) {
const next = event.target.value;
startTransition(() => {
setFilter(next);
});
}
return (
<div>
<input defaultValue={filter} onChange={onChange} />
<Suspense fallback={<p>Loading results…</p>}>
<div style={{ opacity: isPending ? 0.6 : 1 }}>
<ResultsList filter={filter} />
</div>
</Suspense>
</div>
);
}go deeper
Know that a <Suspense> boundary shows its fallback again whenever its content starts loading once more, and that React has a way — startTransition — to mark an update as non-urgent so the old UI stays put.
Explain the difference between urgent updates and transitions, and show the code: the setter inside startTransition, and isPending driving a dimmed or disabled state so the UI still signals progress.
Diagnose the flash from a description: identify the re-suspend, name transitions as the fix, and state the limits — first render is not protected, every contributing setter must be in the transition, and stale content is sometimes the wrong thing to show.
Own the policy question: which parts of the product may show stale data during an update and which must blank, how that decision is encoded so every team makes it the same way, and how boundary placement keeps an unsuppressed fallback scoped to a panel rather than a page.
## Why the content vanishes Candidates usually think of `<Suspense>` as an initial-load feature. It is not. A boundary reflects the current state of its subtree at all times: if anything inside it becomes suspended, it shows the `fallback` — whether that is the first render or the fiftieth update. So the sequence is exactly what you would predict. The user types a filter, you call a state setter, the list re-renders, the list reads data for the new filter, that data is not there, the component suspends, the nearest boundary swaps in the fallback. The perfectly good list the user was reading disappears and is replaced by a spinner, and then reappears. That flash is the single most-asked Suspense bug. ## What happens to the hidden content When an already-revealed boundary re-suspends, React does not throw the subtree away. It hides the existing DOM rather than unmounting the components, so their state survives and is intact when the content is revealed again. Layout effects in the hidden tree are cleaned up while it is hidden and re-run when it comes back, which is why an integration that measures the DOM has to tolerate being torn down and set up again around a re-suspend. State preservation is a real comfort here, but it does not help the user — hidden is hidden, and the flash still happened. ## The fix: mark the update as a transition React separates urgent updates from transitions. An urgent update must be reflected immediately (typing into an input); a transition is an update the user will accept taking a moment (showing the results of what they typed). Suspense treats them differently on purpose: **during a transition, React will not replace already-revealed content with a fallback.** ```jsx const [isPending, startTransition] = useTransition(); function onFilterChange(next) { startTransition(() => { setFilter(next); }); } ``` Now the state change is non-urgent. React renders the new list in the background; while it is suspended, the previously committed list stays on the screen. When the data is ready, React commits the new list in one step. The user sees the old results, then the new results — never a spinner in between. The standalone `startTransition` import behaves the same way for the fallback rule; `useTransition` additionally gives you `isPending`. ## Show progress without hiding anything Suppressing the fallback means you must supply a *different* affordance, or the UI looks frozen for the duration: ```jsx <div style={{ opacity: isPending ? 0.6 : 1 }}> <ResultsList filter={filter} /> </div> ``` Dimming the stale region, disabling the control that triggered it, or showing a thin inline progress bar all read as "working on it" while keeping the content readable. `useDeferredValue` is the other route to the same effect for a value you derive rather than set: React keeps rendering with the previous value until the new one is ready. ## The limits of the suppression Three qualifications matter, and mentioning them is what separates a solid answer from a complete one. **It only protects content already revealed.** On first load, or for a boundary that has never shown its children, the fallback still appears during a transition — there is nothing to keep on screen. **The update must genuinely go through the transition.** If some other, urgent setter also fires as part of the same interaction and drives the same suspending read, that urgent update wins and the fallback comes back. Everything that feeds the suspending render has to be in the transition. **Stale content is still stale.** The user is looking at results for the old filter while the new ones load. For a search results list that is exactly right. For something where showing outdated data is misleading — a balance after a transfer, a permissions screen — a visible fallback is the honest choice, and you should keep it. ## When the fallback is the right answer If the new content is a different thing rather than a new version of the same thing — navigating from a product page to a settings page — showing the fallback is correct, because there is no meaningful "previous version" to keep. Boundary placement helps here too: keeping the boundary tight around the region that actually changes means even an unsuppressed fallback replaces a panel rather than the page.
- Does wrapping the setter in startTransition help on the very first load of that list?No. The suppression rule only protects content that has already been revealed. On the first render of that boundary there is no previous UI to keep, so the fallback shows regardless. Transitions change what happens on *updates*; the initial fallback is exactly what you want there anyway.
- You wrapped the setter in startTransition but the fallback still flashes. What would you check?Whether every state change that feeds the suspending render went through the transition. One urgent setter in the same handler — a scroll reset, a page counter — is enough to make the render urgent again and bring the fallback back. Also confirm the component really is re-suspending rather than being unmounted and remounted, for example by a changing key.
- When is showing the fallback on an update the right choice rather than a bug to suppress?When the stale content would mislead. Keeping the old list on screen is fine for search results, but showing a pre-transfer balance or a previous user's permissions while new data loads is worse than a placeholder. It is also right when the update is a navigation to genuinely different content, where there is no previous version of the same thing to keep.
saying these in an interview costs you the question
- Thinks Suspense fallbacks only appear on the initial render
- Adds a debounce and calls the flashing fixed
- Believes the suspended subtree unmounts and loses its state
- Assumes startTransition also removes the first-load fallback
- Suppresses the fallback but shows no pending affordance at all