HardArchitecture
Design authenticated routing so users never see a flash of the wrong UI on load or on redirect
Model auth as three states, not two: unknown, authenticated, anonymous. Render neither the app nor the login page while it is unknown — render the shell or a skeleton instead. Resolve the state as early as possible: read an httpOnly session cookie server-side and send the answer down with the document so the first paint is already correct, rather than discovering it in an effect after hydration. Guard at the route level so a protected route is never rendered for an unresolved user, and redirect with a replace so the back button does not land the user on the page they were just bounced from.
The flash comes from treating "not logged in yet" and "logged out" as the same value, so the first render is a confident wrong answer. The server-side resolution is what removes the flash entirely rather than hiding it behind a spinner — anything resolved client-side has already painted once. Two details usually missed: preserve the attempted URL so the user lands where they meant to go after logging in, and handle the session expiring mid-session, which is the same three states again but arriving from the other direction.
EasyBest practice
When should a component be split, and what is a bad reason to split one?
Good reasons: a distinct piece is reused elsewhere; a section has its own state that would otherwise re-render the whole parent; the file has become hard to navigate because two unrelated concerns share it; or a part needs its own loading or error boundary. Bad reasons: a line-count rule, or splitting purely to make a file "look clean" — that produces a trail of single-use components that must be opened in sequence to understand one screen, which is harder to read, not easier. The useful test is whether the extracted piece has a name that means something to the product, rather than `CardTop` and `CardBottom`.
The failure mode of splitting by line count is a trail of single-use components that must be opened in sequence to follow one behaviour — the file got shorter and the code got harder to read. A useful test is whether the extracted piece can be named for what it does rather than where it sits; if the best name is FormSectionTwo, the split is arbitrary.
MediumWrite code
Build a `useDebouncedValue` hook, then explain what makes something worth extracting into a custom hook at all
Hold the debounced value in state, and in an effect keyed on `[value, delay]` set a timeout that writes the new value, returning `clearTimeout` as the cleanup. Because the cleanup runs before each re-run and on unmount, a rapid sequence of changes cancels every pending write and nothing fires after teardown. On extraction: a custom hook earns its place when the LOGIC is reused or when a component has several interleaved effects that each become clearer alone. Extracting a single `useState` behind a hook adds a layer without removing anything.
The cleanup is the whole hook. Ask what happens without it: an unmounted component receives an update, which in older React logged a warning and in any version signals a bug in the mental model.
HardCode review
Review this form component
function Form({ user, onSave }) {
const [values, setValues] = useState(user);
useEffect(() => { setValues(user); }, [user]);
const handle = (e) => setValues({ ...values, [e.target.name]: e.target.value });
return <form onSubmit={() => onSave(values)}>{/* fields */}</form>;
}
Several real issues. (1) The effect that syncs `user` into state will silently discard a user’s in-progress edits whenever the prop updates — a refetch mid-typing wipes the form. Resetting via a `key` on the component is the standard alternative, and makes the intent explicit. (2) `handle` closes over `values`, so two updates in the same tick lose one; use the updater form. (3) `onSubmit` never calls `preventDefault`, so the page navigates. (4) There is no submitting or error state, so a slow save is invisible and double-submits are possible. (5) `values` is initialised from a prop, so the component silently assumes `user` is defined on first render.
The prop-syncing effect is the interesting one: it looks like careful code and is in fact the mechanism that loses user input.
MediumConcept
Why does moving a component to a different position in your JSX reset its state, even with identical props?
React identifies a component by its POSITION in the rendered tree plus its element type — not by which variable it came from. If the same component renders at the same position with the same type, React keeps the existing instance and its state. Change the type at that position, or render it at a different position, and React unmounts the old instance and mounts a fresh one, discarding state. This is why a conditional that renders `<Input />` in one branch and `<div><Input /></div>` in the other loses what the user typed: the input is no longer at the same position. It is also why passing a different `key` is the deliberate way to FORCE a reset — changing the key makes React treat it as a different element.
This is the practical half of reconciliation: not "React diffs a virtual DOM" but the specific rule that decides whether your component keeps its state. It explains both an infuriating bug class and the intentional remount-by-key trick.
EasyDebugging
A "Clear" button stops working after the list is filtered once. Find the cause
const clear = useCallback(() => setItems(items.filter((i) => i.pinned)), []);
The empty dependency array freezes the closure at the first render, so `items` inside `clear` is forever the original array. After any change, clicking the button restores that stale list rather than filtering the current one. Fix with the updater form — `setItems((prev) => prev.filter((i) => i.pinned))` — which needs no dependency on `items` at all and keeps the callback genuinely stable. Adding `items` to the deps also works but recreates the callback on every change, defeating the memoization.
The updater form is the idiomatic answer because it solves correctness and stability at once: the callback no longer closes over items, so it needs no dependency on it and stays referentially stable for memoized children. Adding items to the deps also fixes the staleness, but it recreates the callback on every change, which quietly defeats the memoization the useCallback was there to provide.
MediumFollow-up
You virtualised the long list. How would you test it, given most rows no longer exist in the DOM?
Stop asserting on "all rows render" — that is now false by design. Test the contract instead: given a scroll offset, the expected window of rows is present; the total scroll height reflects the full dataset; scrolling to an offset renders the right items; and keyboard navigation can still reach items outside the current window. Set an explicit container height in the test environment, because jsdom reports zero height and a virtualiser will then render nothing, which is the most common reason these tests mysteriously find no rows. For the behaviour users care about, an end-to-end test in a real browser is more honest than a jsdom unit test.
The jsdom zero-height detail is the practical trap, and knowing it signals someone who has actually tested a virtualised list.
HardMultiple choice
What is the real risk of omitting a value from an effect's dependency array to "stop it re-running"?
The dependency array is a claim about what the effect reads. Omitting a used value does not freeze it — it makes the effect operate on an outdated copy. The right fixes are restructuring (updater form, ref, moving the function inside) rather than lying to the linter.
The remaining 58 React questions — plus mock interviews, spaced revision and progress tracking — are in the free interview prep workspace.