A React component's body contains `const Chart = lazy(() => import('./Chart'))`, and `<Chart />` is rendered below a search input. Typing in the input makes the Suspense fallback flash and resets the chart's zoom state every keystroke. Why, and what is the fix?
answer
- state loss plus a flash together
- what does lazy return each call
- element type identity at that position
- the chunk is not re-downloaded
- where the declaration belongs
basics
~20 sCalling lazy during render creates a brand-new component type on every render. React sees a different type at that position, unmounts the old subtree and mounts a fresh one, which loses state and suspends again. Move the lazy call to module scope.
solid answer
~50 s`lazy` returns a new component object every time you call it. Because the call sits in the component body, each keystroke re-runs it and produces a different component *type* at that position in the tree. React compares element types when reconciling, so a changed type is not an update — it unmounts the whole existing subtree, destroying its state and effects, and mounts a new one. The new lazy has no cached payload, so it calls the loader again and suspends, which is the fallback flash; the underlying `import()` resolves from the module cache almost immediately, which is why it looks like a flicker rather than a real load. The fix is to hoist `const Chart = lazy(() => import('./Chart'))` to module scope so the type is created once and stays stable for the life of the module.
code
javascript · 16 linesimport { lazy, Suspense, useState } from 'react';
// Stable type: created once when the module is evaluated.
const Chart = lazy(() => import('./Chart'));
export function Dashboard() {
const [query, setQuery] = useState('');
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<Suspense fallback={<p>Loading chart…</p>}>
<Chart query={query} />
</Suspense>
</>
);
}go deeper
Remember the placement rule: the lazy call goes next to your imports, at the top of the file, never inside a component body. Recognise a flashing placeholder plus lost state as a remount symptom.
Explain that lazy allocates a new component type per call and that React remounts when an element's type changes at a position, and note that the flash comes from re-suspending rather than from a second download.
Diagnose it from evidence — effect cleanups firing every parent render, no repeated chunk request — and give a solution that keeps identity stable when the target module depends on a prop, without leaning on useMemo for correctness.
Generalise the rule: any component type manufactured during render breaks reconciliation identity. Decide how the codebase prevents the whole class — lint coverage, a shared registry for dynamic component loading, review conventions about where component types may be created.
## The symptom Two things happen together — a placeholder flashes and component state disappears — and that pairing is the tell. A loading flash alone might be a slow network; state loss alone might be a key change. Both at once means the subtree is being torn down and rebuilt. ## Why a changed type means a remount When React reconciles the tree, it compares the element at each position with the one that was there before. If the element *type* is identical (the same function or component object), React reuses the existing instance: state survives, effects are not re-run from scratch, DOM nodes are patched. If the type differs, React cannot assume anything about the old instance, so it unmounts it — running cleanup for its effects, discarding its hooks state — and mounts the new type from zero. `lazy` is a factory. Every call allocates a fresh object: ```js lazy(loader) === lazy(loader); // false — two distinct component types ``` So a `lazy` call inside a render body produces a different type on every render of that component, and every render of the parent becomes a full remount of the lazy subtree. ## Why the fallback flashes Each freshly created lazy component starts uninitialised. On its first render React calls the loader, gets a promise, and suspends — even though the module itself is already in the JavaScript module registry from the previous mount. The registry caches by specifier, so `import('./Chart')` resolves essentially instantly, but "instantly" still means a microtask later. React has already shown the nearest `<Suspense>` fallback by then, so the user sees a one-frame flicker of the skeleton on every keystroke. That also explains a confusing detail candidates stumble over: the network tab shows only one request for the chunk, yet the loading state keeps reappearing. Nothing is being re-downloaded; a new lazy component simply has to re-suspend on its first read. ## The fix Create the type once, at module scope: ```jsx import { lazy, Suspense } from 'react'; const Chart = lazy(() => import('./Chart')); function Dashboard() { const [query, setQuery] = useState(''); return ( <> <input value={query} onChange={(e) => setQuery(e.target.value)} /> <Suspense fallback={<ChartSkeleton />}> <Chart /> </Suspense> </> ); } ``` Now `Chart` is the same object across every render, the reconciler treats it as an update, and the loader is called exactly once for the lifetime of the module. ## When the component genuinely varies Sometimes which module to load depends on a prop — a plugin name, a chart kind, a locale-specific editor. Creating a lazy component per value inside render is still wrong for the same reason. Two workable shapes: - **A lookup table at module scope.** Build a map from key to lazy component once, then index into it: `const editors = { markdown: lazy(...), rich: lazy(...) }` and render `const Editor = editors[kind]`. Each entry is a stable type, so switching `kind` is a deliberate remount and reusing the same `kind` is a plain update. - **A memoised cache** keyed by the identifier, living outside the component (a module-level `Map` that stores the lazy component the first time each key is seen). This scales when the set of keys is open-ended. What you should not do is reach for `useMemo` and call it done. `useMemo` is a performance hint, not a semantic guarantee — React is allowed to discard a memoised value, and if it does you are back to a remount at an arbitrary moment. Component *identity* is correctness, so it belongs somewhere with real lifetime guarantees: module scope, or a ref if the value must be per-instance. ## Neighbouring versions of the same bug The underlying rule — a component type created during render forces a remount — is not specific to `lazy`. Defining a plain function component inside another component's body, or building a wrapper component on the fly, produces exactly the same unmount-and-remount behaviour, minus the loading flash. Recognising the shared cause is what an interviewer is really probing: the candidate who says "the type identity changed" has understood reconciliation, while the candidate who says "React re-downloads the chunk" has not. ## How to confirm it in practice Mount the subtree and watch its effects: cleanup running on every parent render is decisive evidence of a remount. React DevTools showing the lazy component's state resetting, with no repeated network request for the chunk, points at the same conclusion. Once you see remount-per-render, the search is for a component type being manufactured during render — and `lazy` in a function body is the classic offender.
- Would wrapping the lazy call in useMemo fix it?It hides the symptom but is not a correctness guarantee. `useMemo` is a performance hint — React may discard the cached value, and if it does the component type changes and the subtree remounts at an unpredictable moment. Component identity is a correctness property, so put the `lazy` call at module scope, or in a module-level map when the target module depends on a prop.
- The network tab shows only one request for the chunk, so why is there a loading state at all?Because suspending is a property of the lazy component instance, not of the network. A newly created lazy starts uninitialised, calls the loader, and suspends on the returned promise even when the module registry already has the module. The promise settles a microtask later, so nothing is refetched — but React has already swapped in the Suspense fallback, and that is the flash you see.
- How do you pick a lazy component based on a prop, without recreating it every render?Build the lazy components once outside the component — a module-scope object mapping each key to its lazy component, or a module-level Map that memoises them the first time a key appears. Render `const Editor = editors[kind]`. Each key then has one stable type, so re-rendering with the same `kind` is an update, and changing `kind` is an intentional remount.
saying these in an interview costs you the question
- Claiming the chunk is re-downloaded on every render
- Saying Suspense resets its children's state by design
- Reaching for useMemo and calling identity solved
- Thinking lazy returns the same component for the same loader
- Blaming StrictMode's double render for the state loss