Everything the adaptive question bank can teach and test in React, from foundations through advanced practice. Work through it in order, or start practicing and let the questions find your level.
Capitalization is not style here — it is how the compiler decides what to emit. A lowercase name becomes a string tag and renders a DOM element of that name; a capitalized one becomes a reference to your function. Rename a component to lowercase and React quietly renders an unknown HTML element instead of erroring.
A function returns one value, and JSX is no exception, so two adjacent tags are a syntax error. A fragment groups them without adding a wrapper to the output, which matters wherever a stray div would break the layout — a flex row, a table body, a grid. Use the long form when you need a key on it.
Every element closes, including the ones HTML lets you leave open. It is the commonest error when pasting markup from elsewhere, and it fails at build time rather than rendering something odd, which is the good outcome. Self-close anything with no children.
JSX attributes are named after the DOM property rather than the HTML attribute, which is why class and for — both reserved words in JavaScript — appear as className and htmlFor. The exceptions are worth memorizing: aria-* and data-* keep their hyphens, because they have no property form.
The braces hold an expression, meaning something that evaluates to a value. That rules out if, for and switch, which is why conditional JSX is written with a ternary or with logical operators. When the logic outgrows an expression, compute it above the return and interpolate the result.
No special syntax is involved: the outer braces open an expression slot and the inner ones are an object literal. Seeing it that way explains why a style value is camelCased and why a variable can be passed instead. The same doubling appears any time a prop takes an object.
Markup compiles to an ordinary object describing what to render, so it can be assigned, put in an array, passed as a prop or returned from either branch of a condition. Treating JSX as data rather than as a template is what makes patterns like a slot prop or a lookup table of views possible.
Whatever sits between the opening and closing tags arrives as props.children. This is the primary composition tool: a card, a layout or a modal accepts arbitrary content without knowing anything about it, so it never needs a prop per possible variation.
Returning null means the component produced no output, not that it was skipped. It still rendered, its Hooks still ran in order, and its state is intact. That distinction matters because it is why you cannot avoid a Hook by returning early above it.
&& returns its left operand when that operand is falsy, and React renders the number zero rather than ignoring it — so an empty array's length prints a stray 0 on screen. Use a ternary, or coerce the test to a boolean. Empty string and null do not have this problem, which is why it surprises people.
Interpolated values are escaped before they reach the DOM, so user text containing tags renders as text. That is the default, and it is why React apps are not routinely vulnerable to injected markup. The opt-out is deliberately named to be uncomfortable to type.
One default per file, as many named ones as you like, and the import spelling differs — braces for named, none for default. The asymmetry worth knowing: a default import can be renamed at will by whoever imports it, so two files can refer to the same component by different names and neither is wrong.
B. Props and composition
A component receives props and must treat them as frozen; the only thing that can change them is the parent re-rendering with new values. Writing to a prop object is the mutation that produces a UI showing stale data, because nothing scheduled a render. If a child needs to change a value, it needs a callback.
A destructuring default fires only when the value is undefined. Pass null, 0 or an empty string and the default is skipped and the falsy value is used, which is usually the bug — a caller clearing a field with null gets null, not the fallback. Use ?? when you want the broader test.
Forwarding every prop is convenient and erases the component's interface: nothing at the call site or in the definition says which props matter. It also forwards typos silently. Spreading is right for a thin wrapper over a DOM element and wrong for a component with real behavior.
When a prop only exists to be handed further down, the intermediate components are carrying something they do not use. Accepting children instead lets the owner build the content and pass it through in one piece, which removes the chain and usually removes a re-render problem too.
A function passed as a prop gives the child control over when it runs, how often, and with what arguments — so the child owns the behavior while the caller owns the markup. It is the pattern behind most headless components, and a custom Hook is often the simpler modern alternative.
React re-renders the whole subtree by default; unchanged props do not stop it. This is usually fine, because rendering is cheap and the commit only writes real differences. It becomes a problem at scale, which is what memo and passing content as children exist to address.
A key tells React which item is which between renders, so it can match state and DOM nodes to the right row. It must be unique among siblings and stable across renders — an id from the data, not something generated during render. Generating a key while rendering defeats the entire mechanism.
With an index key, inserting at the front shifts every item's key by one, so React matches row zero's state to what is now row one. The list looks right and the state is wrong — a checked checkbox follows the position instead of the item. It is safe only for a list that never reorders, inserts or deletes.
React reads key itself and strips it, so the child sees nothing. A component that needs the value must receive it under another name as well. The same is true of ref in older versions, and the error is silent: you read props.key and get undefined.
A higher-order component takes a component and returns a new one, so the shared behavior lives in a wrapper. That adds a real node to the tree — visible in DevTools, affecting refs and display names — where a custom Hook shares logic without changing the tree at all.
A parent and its children coordinate through context rather than through props, so the consumer arranges them freely instead of configuring one component with a growing list of options. The cost is that a child is meaningless outside its parent, which the API has to make obvious.
Rather than adding an option for each new case, hand the decision back: let the caller supply the reducer, spread prop getters onto their own elements, or render their own markup around your behavior. It keeps the component small while widening what it supports.
One component, many underlying elements: the behavior and styling stay put while the caller decides whether it renders as a button, an anchor or a list item. It matters most for accessibility, because the right element carries the right role and keyboard behavior for free.
C. State: a component's memory
The array is positional, so the names are yours and the order is fixed: value first, setter second. One call per independent value — there is no object form — and the call must happen unconditionally at the top level, because React matches Hooks to their state by call order.
Rendering a component twice creates two independent stores of state. The function is shared; the state is not. This is why a list of rows each with their own open/closed flag needs no coordination, and why a module-level variable is the wrong place to keep anything per-instance.
The argument is read on the first render and ignored afterwards, so passing a prop as the initial value does not track that prop. The state is now independent of it. When you genuinely want a reset on a prop change, change the component's key instead.
Passing the function lets React call it once, on mount. Passing the call runs it on every render and throws the result away every time but the first — invisible when it is cheap, and a real cost when it parses storage or builds a large structure. The difference is one pair of parentheses.
The setter does not assign to your variable; it asks React to render again with a new value. The local binding keeps whatever it had for the rest of this render, which is why reading it straight after setting it shows the old value. That is the behavior, not a race.
Within one render every state value is a fixed number, so three calls that each compute from that number all compute from the same one. The result is one increment, not three. This one fact explains most surprises about setters, and the updater form is the way around it.
Passing a function queues a transformation rather than a value, so React applies each in turn against the result of the last. Three queued increments genuinely add three. Reach for it whenever the next value depends on the current one, especially inside a handler that fires more than once.
Updates in the same tick are collected and applied in one re-render, so the UI never shows an intermediate state. Since React 18 this includes updates inside promises, timeouts and native handlers, which previously escaped batching. Rely on it rather than trying to coalesce updates yourself.
Setting state to a value that is already there lets React skip the work. The comparison is by identity, so a new object with identical contents is not equal and does re-render — which is the usual reason a component keeps rendering when nothing visible changed.
Calling your own component's setter while rendering is legal and is the supported way to adjust state when a prop changes. React discards the output and re-runs immediately, before anything is committed, so nothing flashes. Calling another component's setter during render is not allowed.
The inner function is created fresh on each render, so React sees a different component type every time and unmounts the old subtree rather than updating it. State is lost, Effects re-run, and inputs lose focus — all of which read as a mysterious reset. Move the definition to module scope.
State belongs to a position in the tree, not to your JSX. Keep the same component type at the same position and the state survives; render it somewhere else in the tree and it is discarded and rebuilt. Both halves are useful once you know which one you are relying on.
D. Render, commit, and reconciliation
Three distinct steps, and separating them explains almost everything else. A mount or an update TRIGGERS; React calls your components to work out what the output should be, which is the RENDER; then it applies the differences to the DOM, which is the COMMIT. Your code runs in the middle step only.
React compares the new output with the last and touches only what differs, so a node whose attributes are unchanged is not rewritten. That is why scroll position, focus and a playing video survive a re-render — the elements were never replaced, only the changed parts were.
The browser paints once the commit is finished, which is why an ordinary Effect sees the screen the user already saw. useLayoutEffect runs in the gap between commit and paint, so a measurement and correction there is invisible — at the cost of holding the frame.
A re-render is a function call, and if it produces the same output no DOM work follows. People chase re-render counts assuming each one costs a repaint; most cost a comparison. Measure the commit before optimizing the render.
React matches the previous tree to the new one by element type at each position. Same type, same slot: update in place and keep the state. Different type: destroy the old subtree, state included, and build the new one. Conditional rendering that swaps types is therefore also swapping state.
Adding a wrapper element around a component moves it to a different position in the tree, so its state is discarded even though its own JSX did not change. Conditionally wrapping a subtree is the usual way this happens; render the wrapper unconditionally and vary its props instead.
Because state is keyed to position, giving a component a new key at the same position tells React it is a different thing — the old one unmounts and a fresh one mounts. It is the intended way to reset a form when the record being edited changes, and it is simpler than clearing every field.
Given the same props and state a component must return the same JSX, and must not modify anything that existed before this render. Purity is what makes the other guarantees possible: React may call a component twice, drop the result, or render it in the background.
In development React deliberately calls components twice so that impure code shows itself — a counter incremented during render visibly jumps by two. It is a diagnostic, not a bug, and production renders once. Silencing it by moving the mutation into a ref hides the problem rather than fixing it.
E. Updating objects and arrays immutably
Mutating an object that is already in state changes the data and tells React nothing, so no render is scheduled and the screen keeps showing the old view. Worse, the value React compares against has changed too, so even a later correct update can be skipped as identical.
Build a new object with the change applied and pass that to the setter. The spread is shallow: nested objects are shared with the original, so mutating one of those has exactly the problem you were avoiding. Copy at every level you intend to change.
Only the objects on the path from the root to the edited field need copying; everything off that path can be shared. Deeply nested state makes this verbose, which is itself the signal — flattening the shape usually removes the problem rather than making it easier to write.
Half the array methods edit in place and half return something new, and the names give no hint. push, pop, splice, sort and reverse mutate; map, filter, slice, concat and the newer toSorted and toReversed return a copy. Only the second group is safe on state.
Removal is a filter with a predicate that excludes the item. Insertion is spreading the slice before the index, the new item, then the slice after. Both produce a new array, which is the only reason React notices the change at all.
Map over the array returning a modified copy for the item you are changing and the original object for every other. Returning copies of the untouched items works too, and costs you every memoised row re-rendering for nothing.
Dependency arrays, memo and store selectors all compare by reference. An in-place edit leaves the reference identical, so every one of those mechanisms concludes nothing changed. This is why immutability is not a style preference here — it is the signal the whole system runs on.
A producer library lets you write what looks like mutation against a draft and hands back a genuinely new object, sharing everything you did not touch. It removes the nested-copy verbosity without giving up the identity guarantees the rest of React depends on.
F. Choosing, lifting and sharing state
If two values are always set together, keeping them apart invites the render where one has updated and the other has not. One object, one setter, one consistent value. The reverse is also true: values that change independently should not share a state variable.
Two booleans give four combinations and usually only three are legal, so the illegal one is a bug waiting for a user to find it. A single status value with named states cannot represent the impossible case, and reading the code tells you exactly which states exist.
Anything you can calculate from props or other state should be calculated during render, not stored. A stored copy can disagree with its source, and keeping the two in step is exactly the Effect you should not be writing.
Hold the selected id rather than a second copy of the selected object. With a copy, editing the item in the list leaves the selection showing the old values, and nothing makes that obvious. An id forces the lookup to happen against the one real source.
Storing items in a map keyed by id, with arrays of ids for structure, turns a deep update into a change at one node. It also removes most of the nested copying and makes the identity of an unchanged item stable, which every memoised consumer benefits from.
A const computed during render is correct by construction: it cannot be stale, because it is recalculated from whatever the current values are. An Effect that writes derived state renders once with the old value, then again with the new, and is stale in between.
When two components need the same value, move it to their closest common ancestor and pass down the value plus a way to change it. The parent becomes the single source of truth; the children become predictable functions of what they are given.
A controlled component takes its value and its change handler from above and owns nothing; an uncontrolled one keeps its own state and tells you about changes. Both are valid designs, and a component that is one in some places and the other in others is the one to avoid.
Setting value without onChange pins the field to that value, so typing appears to do nothing — React resets it on every keystroke. Either supply the handler or use defaultValue, which sets the initial text and then leaves the input alone.
State held above its only consumer re-renders everything in between for no benefit. Push it down to the component that actually reads it, and lift it only when a second consumer genuinely appears. Lifting speculatively is how a tree ends up re-rendering on every keystroke.
G. Reducers
A reducer takes the current state and an action and returns the next state — nothing else, and no side effects. Because it is a plain function of its inputs it can be tested directly, without rendering anything, which is most of the reason to reach for one.
Name an action for what happened, not for what to assign. An intent-shaped action survives a change in how state is stored; a setter-shaped one has to be rewritten alongside it. It also reads as a log of what the user did, which is what makes a reducer debuggable.
No fetching, no timers, no mutation of the state passed in. React may call it more than once with the same arguments — StrictMode does so deliberately — and a reducer with a side effect will perform it twice. Put the effect where it belongs and keep the transition pure.
React guarantees the same dispatch function across renders, so it never needs to appear in a dependency array and never invalidates a memoised callback. This is a real advantage over passing a handler built during render.
A reducer earns its place when several transitions touch the same state in different ways. It puts them in one place, names them, and makes them testable in isolation. For one independent value it is only ceremony.
The third argument computes the initial state from the second, lazily and once. It also gives you a reset for free: call the same function again from a reducer case. Without it the initial value is rebuilt on every render and discarded.
Dispatching queues a transition; it does not hand back the result. The next state is readable on the next render, so code that dispatches and then reads state sees the old value. If you need the new value immediately, compute it yourself.
H. Context
The default is used when a consumer has no provider above it at all. A provider passing undefined is still a provider, so the consumer gets undefined rather than the default — which is why the default is a poor place for a real fallback and a good place for a value that makes the mistake obvious.
React 19 lets you render the context object directly as the provider. The older spelling still works, so this is a migration rather than a break — worth knowing mainly so both forms read as the same thing when you meet them in one codebase.
A consumer reads the closest provider above it, so nesting one inside another overrides the value for that subtree and leaves the rest untouched. That is how a themed section or a scoped configuration is built, and it is deliberate rather than an accident to guard against.
An object literal written inline is a new object on every render of the provider's parent, so every consumer sees a changed value and re-renders even when the contents are identical. Memoise the value, or split it, or hold it in state.
Context broadcasts one value to every consumer, and any change to it re-renders all of them. There is no way to subscribe to part of it. That is fine for a theme or a locale and poor for state that changes often and is read by many components for different reasons.
Dispatch never changes, so putting it in its own context means components that only trigger actions never re-render when the state moves. It is the cheapest fix for the broadcast problem and costs one extra provider.
A portal changes where the DOM node lands, not where the component sits in the React tree. Providers above it still apply, so a modal rendered into the document body keeps the theme, the router and every other context from its logical parent.
One reducer for the transitions, a context for the state and another for the dispatch. It is the state management React ships with, it needs no dependency, and it is enough until the broadcast cost of context becomes the bottleneck.
Before adding a context, check whether passing children or one more prop would do. Context is invisible at the call site, which is exactly what makes it convenient and what makes a component hard to move or test later.
I. Refs and the DOM
A ref is a mutable box React carries between renders. Writing to it changes the value immediately and schedules nothing, so the screen does not update. That is the point: it is for values the UI does not display.
Reading or writing ref.current while rendering makes the output depend on something outside props and state, which breaks purity and misbehaves under concurrent rendering. Do it in an event handler or an Effect, where the render is already finished.
Ask whether the value appears on screen. Timer ids, the previous value of something, a DOM node, a scratch flag — none of them are rendered, so none of them should trigger a render. Anything the user sees belongs in state.
Putting a ref on a host element gives you the real node after the commit, which is how you focus a field, measure an element or hand a container to a non-React library. It is the sanctioned escape hatch, not a workaround.
During the first render the element does not exist yet, so the ref holds null. Code that reads it must run after the commit — in an Effect or a handler — and should still guard, because the node is null again after unmount.
From React 19 a ref arrives on a function component as an ordinary prop, so passing it down is just passing a prop. That removes the wrapper that existed purely to do this plumbing, and it is the reason new code has no need of forwardRef.
forwardRef existed because ref used to be intercepted by React and never reached the component. Once it became a normal prop the wrapper had nothing left to do. Existing code keeps working; new code should not add it.
A function ref is called with the node when it attaches. In React 19 anything it returns is treated as the detach cleanup, which is a much clearer way to unsubscribe than watching for the null call that used to signal removal.
Instead of handing a parent the raw node, expose a small named API — focus, scrollToTop, whatever the parent legitimately needs. It keeps the internals free to change and makes the imperative surface something you chose rather than everything the DOM offers.
React owns the nodes it renders; changing them by hand means your edit is discarded on the next commit, or the tree and the DOM disagree. Manipulate only nodes React does not manage — an empty container you hand to a chart library, for instance.
J. Effects: synchronizing with external systems
An Effect exists to connect your component to something React does not control: a socket, a subscription, a third-party widget, a browser API. If the code only computes a value from props and state, it belongs in render, and putting it in an Effect adds a render pass and a stale window.
The user sees the committed render before the Effect fires. That is right for most synchronization and wrong when the Effect corrects something visible, because the correction appears as a flash. That case is what the layout variant is for.
No array means after every commit. An empty array means once, on mount. A populated array means whenever one of those values changes by identity comparison. The array is a claim about what the Effect reads, and the linter checks that claim.
Cleanup is not only for unmount: it runs before each subsequent execution too. Writing the Effect as start-and-stop rather than do-this-once is what makes that ordering obvious, and it is what keeps a subscription from accumulating as dependencies change.
In development React mounts, unmounts and mounts again, so an Effect without cleanup immediately shows a doubled subscription or a duplicate request. It is deliberately surfacing the bug that would otherwise appear only after a few navigations in production.
Ask what should be true while the component is on screen, and how to undo it — not what should happen at mount and update. The first framing produces a cleanup naturally; the second is how Effects end up with subscriptions nobody closes.
Nothing guarantees responses come back in the order they were sent, so an earlier slow request can land after a later fast one and overwrite fresher data. Set a flag in the cleanup and discard the stale result, or abort. Skip it and the screen intermittently shows data belonging to a previous input.
An object or function created during render is a new value each time, so listing it as a dependency re-runs the Effect on every render. Move it inside the Effect, reduce it to the primitive fields you actually use, or memoise it.
Suppressing the lint leaves the Effect reading a value from the render it was created in, which is the classic stale closure — correct at first and silently wrong after an update. The fix is always to change the code until the dependency is genuinely not needed.
If the Effect only needs the current state in order to compute the next one, the functional updater gives it that without reading the value — so the state leaves the dependency array and the Effect stops re-running on every change.
An Effect Event reads the latest props and state at the moment it is called, without making the Effect re-run when they change. It is the sanctioned way to use a value inside an Effect that should not be a dependency, and it must never be listed as one.
It fires after the commit and before the browser paints, so measuring and adjusting there is invisible to the user. The cost is that the browser is waiting: everything in it is blocking the frame. Use it for measurement, not for data.
It runs earlier still, before any layout Effect reads the DOM, so a style library can inject its rules before anything measures. It cannot read refs and is not intended for application code — it exists for the libraries that generate styles during render.
K. You might not need an Effect
An Effect that reads state and sets other state renders twice for every change and shows the intermediate value in between. Computing the same thing during render produces one pass and cannot be stale. This is the single most common unnecessary Effect.
If a calculation is genuinely slow, cache it — do not move it into an Effect and store the result. The cache keeps the value derived and fresh; the Effect makes it a second source of truth that can disagree. Measure first: most derivations are not slow.
When a prop changes and everything about the component should start over, give it a new key. React discards the instance and mounts a fresh one with its initial state. An Effect that clears each field runs after a render in which the old values were on screen.
When only part of the state should reset on a prop change, compare the prop to a previous value held in state and call the setter during render. React re-runs before committing, so nothing flashes — which an Effect cannot promise.
If something happens because the user did it, put it in the handler. An Effect watching state cannot tell a click from any other path to the same value, so it fires on mount, on navigation and on a restored draft too.
Each Effect in the chain costs a render, and the intermediate states are all briefly visible. If the chain is computation, it is one calculation; if it starts from a user action, it is one handler. Either way it is not several Effects waking each other.
Work that must happen once per application, not once per mount, belongs at module scope or behind an explicit guard. A mount Effect runs twice in development by design, and again whenever the component remounts.
Call the parent's callback where the event happens, with the data. Notifying the parent from an Effect that watches the state splits one action across two renders, and the parent now learns about it a beat later than it happened.
L. Rules of React and custom Hooks
React identifies a Hook by the order it was called in, not by name. Put one behind a condition and the order changes between renders, so the second Hook receives the first one's state. Early returns count too — everything must run, every render, in the same order.
Hooks need a component's render to attach to, which a plain function, a class method or an event handler does not provide. Custom Hooks work because they are called from a component and inherit its render. Calling one from a handler fails at runtime.
The use prefix is what the lint rules and the Compiler use to decide that a function may contain Hooks and must obey the Hook rules. It is a naming convention that tooling depends on: rename a custom Hook without the prefix and the checks stop applying to it.
Two components calling the same custom Hook each get their own independent state — the Hook is a recipe, not a store. Sharing a value between components still needs lifting it up or putting it in a context; a shared Hook will not do it.
Same props and state, same JSX, and nothing outside this render modified along the way. React relies on it to call a component twice, throw a result away, or render in the background — all of which are behaviors a component with side effects breaks.
No requests, no DOM writes, no subscriptions, no pushing onto a shared array while rendering. The render may be discarded or repeated, so any of these happen an unpredictable number of times. Move them to an event handler or an Effect.
This includes values you have already put into JSX, because React may keep and reuse them. Mutating after handing something over produces a tree that disagrees with what was rendered, and the symptom appears somewhere else entirely.
Calling the function inlines its output into the caller: no separate instance, so its Hooks belong to the caller and its state disappears on the caller's next render. Render the element instead and let React create the instance.
use is deliberately allowed inside conditions and loops, which no other Hook is. It does not store per-call state the way the others do, so call order does not have to be stable. Do not generalize the exemption to anything else.
These rules are checked at build time rather than trusted to discipline. A suppression comment is not a fix — it converts a diagnostic into a stale-closure bug that reaches production and is hard to attribute later.
M. Performance, memoization and the Compiler
memo compares each prop by identity against the previous render and skips the component when all of them match. It does not deep-compare and it does not help when the component re-renders because of its own state or a context it reads.
An object, array or arrow function written in the JSX is a new value every render, so the shallow comparison always fails and the memo never skips anything. Wrapping a component in memo while passing an inline callback is work that buys nothing.
It recomputes when a dependency changes and otherwise returns the cached value. React is allowed to discard the cache, so it is an optimization hint, never a guarantee — which is why nothing that must happen exactly once can rely on it.
It is the same mechanism with the function itself as the cached value. Knowing the two are equivalent is the quickest way to reason about them: both exist to keep an identity stable so that something downstream comparing by identity can skip work.
Every memo adds a comparison on each render and holds its cached value in memory. Applied everywhere by default it costs more than it saves and makes the code harder to read. Profile, find the component that is actually expensive, and memoise that one.
The cache can be thrown away and recomputed at any time, so anything with a side effect will happen an unpredictable number of times. If it must happen once, it needs an Effect with a cleanup, not a memo.
Content passed through children is created by the parent's parent, so its element object is unchanged when the middle component re-renders — and React skips it without any memo at all. Restructuring to pass children is often cheaper than memoising.
The React Compiler inserts memoisation during the build, sized to what the code actually reads. Where it applies, hand-written memo, useMemo and useCallback are mostly redundant, and removing them is a simplification rather than a regression.
It can only optimize code whose behavior it can prove, so a component that mutates, reads a ref during render or calls something it cannot analyze is skipped. Purity stops being advice and becomes the condition for getting the optimization.
In annotation mode the Compiler only touches functions that opt in, and the opt-out directive excludes one it would otherwise handle. They are the tool for incremental adoption and for the occasional function whose behavior depends on not being cached.
React's own tracks in a browser profile group work by scheduling priority — blocking updates separated from transitions — and show the gap between when an update was scheduled and when it rendered. That gap is the thing to look at; a flame chart alone does not show it.
Wrapping a subtree reports how long its render took and how long it would have taken without any memoisation, which is the comparison that tells you whether a memo is paying for itself. It only reports in development builds.
Render the rows in view plus a small buffer and let the rest be height. The cost then follows the size of the screen rather than the size of the data, which is the only approach that keeps a ten-thousand-row list usable.
N. Transitions, deferred values and external stores
Marking an update as a transition tells React it is not urgent, so an urgent update — a keystroke, a click — can interrupt and be handled first. The expensive re-render then restarts. This is how a heavy filtered list stops making the input feel stuck.
useTransition gives you a flag scoped to the component that called it, so you can show a spinner exactly where the slow thing is while the rest of the page stays responsive. It is the reason to prefer the Hook over the standalone function.
The non-Hook form can be called anywhere, including outside a component, which is what a store or a router needs. What it cannot give you is a pending flag, so any loading indicator has to come from elsewhere.
React 19 allows the transition function to be async, so the pending state covers the whole await rather than ending at the first suspension point. That single change is what makes an Action a coherent unit: submit, await, settle, with pending state throughout.
If an update inside a transition suspends, React keeps the current screen visible instead of replacing it with a fallback. That is usually what you want on navigation: the old page stays until the new one is ready, rather than flashing a spinner in between.
It hands back the previous value immediately and re-renders with the new one in the background. The urgent part of the UI updates at once and the expensive part catches up, without you having to split the state yourself.
On the very first render there is no previous value to fall back to, so the optional second argument supplies one. Without it the first render uses the real value and does the expensive work you were trying to defer.
The distinction is who owns the change. If you were handed a value and want the expensive consumer to lag, defer it. If you are triggering the update yourself, wrap it in a transition. Reaching for the wrong one usually means fighting the API.
Concurrent rendering can pause mid-tree, and if an external mutable store changes during that pause, two components read two different values into one commit. The UI then shows inconsistent data with no error. React's own state cannot tear; anything outside it can.
You supply a way to subscribe and a way to read the current value, and React handles the consistency. It is the supported way to read any store React does not own, and library authors are the main audience — most applications get it through a library.
The snapshot is compared by identity, so building a fresh object on each call looks like a change every single time and the component renders without end. Hold one value and hand back that same reference until the underlying store genuinely moves.
Server rendering has no subscription and no client store, so the third argument supplies the value to use there and during hydration. Omit it and a server-rendered component using the store throws.
It leaves a subtree mounted, state and all, while marking its work low priority — so a hidden tab can be rendered ahead of time rather than destroyed and rebuilt on every switch. Reach for it when recreating that state is the expensive part.
O. Suspense, lazy and the use Hook
The boundary displays its fallback while anything beneath it is waiting, and swaps back once everything resolves. It is declarative: the components below do not know they are inside one, and the loading UI lives in one place rather than at every fetch.
Lazy components, use on a promise, and data from a framework that integrates with it. An ordinary fetch inside an Effect does not suspend — it sets state — so wrapping it in a boundary achieves nothing. Knowing the list saves a lot of confusion.
Calling lazy() during render produces a new component type each time, so React remounts the subtree on every render and it never settles. Declare it once at module scope, where it is created a single time.
Everything inside the boundary is replaced while any one thing inside it waits, so siblings that were ready disappear too. Where you place a boundary is therefore a design decision about what may vanish together, not just a technical one.
An inner boundary lets a fast section render while a slow one is still pending, instead of holding the whole page to the speed of its slowest part. This is how a page reveals progressively rather than all at once.
use suspends the component when given a pending promise and returns the resolved value when React retries. Unlike the other Hooks it may be called conditionally, which is what lets it sit inside a branch that only sometimes needs the data.
A promise built during render is new on every attempt, so the retry suspends again and the component never completes. The promise must come from somewhere stable — a cache, a framework loader, or a Server Component that passes it down.
Given a context, use behaves like useContext but may be called inside a condition or a loop. It is the one way to read a context conditionally, and it is why use is described as a special form rather than an ordinary Hook.
The server sends the shell first and streams each boundary's HTML as its data resolves, with React swapping the content in place of the fallback. The user sees the page fill in, and none of it waits on the client bundle.
Suspense handles pending, not failed. A rejected promise propagates as a thrown error and needs an error boundary above it; without one it reaches the root and blanks the tree. Pair the two whenever data can fail.
P. Forms, Actions and optimistic UI
Passing a function to action hands it the FormData on submit, with no preventDefault needed, and React resets uncontrolled fields afterwards. That reset is the part that surprises people — it is deliberate, and it is why the pattern suits create-style forms.
An Action is managed by React: it gets pending state, it integrates with transitions, and because the form element carries it, submission can work before hydration finishes. A submit handler is ordinary JavaScript and does none of that.
It returns the last result, a wrapped action to put on the form, and a pending flag. Wrapping an action this way turns whatever it returns into state, which is how a server-side validation error gets back onto the screen.
Under useActionState the action is called with the previous state first and the form data second. Writing the parameters the other way round is the usual mistake, and it fails in a confusing way because both arguments are objects.
It reads the status of a <form> above it in the tree, so calling it in the component that renders the form returns nothing pending. The submit button has to be its own component for the Hook to see anything.
It shows the expected result straight away while the action runs, and reverts by itself once the action settles — including on failure. The value is derived from the real state, so there is no separate copy to keep in step.
It lives exactly as long as the action does. Once that settles, the real state takes over — whether it confirms the guess or contradicts it. Trying to write the optimistic value back somewhere permanent misreads what the mechanism is for.
FormData is built from the form's named fields, so an input without a name is simply absent from it. It is a silent omission — no warning, just a missing key — and a common cause of a field that always submits as empty.
Switching value from a string to undefined turns a controlled input uncontrolled mid-life, and React warns because the field will now keep its own state. It usually means a value arrived as undefined from data that had not loaded.
React sets the current selection through value on the <select> and the text through value on the <textarea>, rather than through selected on an option or content between the tags. It makes both behave like every other controlled input.
A checkbox or radio is controlled through checked, not value — value is the string it submits. Binding state to value produces a control that never changes appearance no matter what you set.
Validating the whole form against one schema keeps the rules in a single place and gives you types for free. The design question is when it runs: on every keystroke it nags, on submit alone it surprises, and on blur it usually lands somewhere sensible.
Rows the user can add and remove are a list, so they need a stable identity each — generated when the row is created, not derived from its index. Without it, removing a row moves another row's value and focus onto the wrong field.
Q. react-dom: events, portals and resources
React hands your handler a normalized event object so behavior matches across browsers, with the underlying browser event available when you need something React does not wrap. Most code never needs the original; the cases that do are usually about a newer DOM API.
Listeners are attached to the root container rather than to each element, so React hears events as they bubble up. It keeps attachment cheap, and it explains the surprises when React is mixed with hand-attached listeners: a native stopPropagation outside React can prevent React from ever seeing the event.
Capture handlers run during the descent, ahead of every bubbling handler. That ordering is what an interceptor needs — logging, or dismissing a menu when anything within it is clicked — because it has to act before the specific handler gets its turn.
They solve unrelated problems. One cancels what the browser would have done — following a link, submitting a form. The other stops the event traveling further up the tree. Reaching for the wrong one is why a form still navigates or a parent handler still fires.
onClick={handleClick} registers the function; adding parentheses calls it during render and registers whatever it returned, usually undefined. The handler appears to fire immediately and then never again. When you need arguments, wrap it in an arrow.
The style prop takes an object with camelCased keys, and a bare number is treated as pixels for the properties that take a length. That is convenient and occasionally wrong — lineHeight and zIndex are unitless, so a number there means what it says.
The DOM node moves, usually to the body; the component's place in the React tree does not. That is how a modal escapes an ancestor's clipping or stacking context without being torn out of the hierarchy it logically belongs to.
Because the component's position in the React tree is unchanged, an event from inside a portal bubbles to its React parent rather than to its DOM parent. A click inside a portalled modal therefore reaches the handler on the component that rendered it, which is usually what you want and occasionally a surprise.
It commits immediately rather than letting React batch, so the DOM is up to date on the next line and you can measure it. It also defeats batching and blocks the frame, so it is an escape hatch for a specific measurement, not a way to make state feel synchronous.
React 19 moves <title>, <meta> and <link> rendered anywhere in the tree into the document head. A page-level component can therefore declare its own metadata next to the content it describes, without a separate helmet-style library.
Marking a stylesheet link with a precedence lets React order it relative to others and deduplicate it when several components ask for the same sheet. It gives component-owned stylesheets a defined cascade order instead of whatever order they happened to mount in.
preload fetches the resource and stops there; preinit fetches and evaluates it. The difference matters for a script or a stylesheet you will certainly need — preloading saves the round trip, preiniting also pays the execution cost up front.
R. Client roots, hydration and StrictMode
A root ties React to one DOM container, and rendering into it mounts the tree. Everything about concurrent rendering lives on the root, which is why an app created with the legacy entry point silently opted out of it.
Calling render on a root that already has a tree updates it rather than starting over, so state survives. It is rarely needed in an app that has a single mount, and is exactly what an incremental migration or an embedded widget relies on.
Unmounting tears the tree down and runs every cleanup, which is how a React island embedded in another page removes itself properly. The root cannot be reused afterwards — a later render needs a new one.
Hydration adopts the server's markup and attaches behavior to it, instead of rebuilding the DOM. That is why the first client render has to produce the same output: React is matching, not replacing.
Anything that differs between the server and the browser breaks the match — a current time, a random value, locale-dependent formatting, a check for window. The fix is to render the same thing on both and adjust in an Effect, after hydration.
A one-level opt-out for a difference you cannot avoid, such as a rendered timestamp. It silences the warning for that element only and does not cascade, which is deliberate — it is not a way to make a whole subtree's mismatches acceptable.
React 19 logs a single readable diff and re-renders the affected subtree on the client, rather than patching quietly and leaving you with a tree that disagrees with itself. The log is the point: the mismatch is still a bug worth fixing.
The identifier it produces matches between a server render and the hydration that follows, so a label-to-input or aria-* association survives intact. It is not a list key: the value is derived from position in the tree, not from your data.
A root option that reports the errors React handled without crashing, hydration re-renders among them. Without it those recoveries are invisible in production, which means a systematic mismatch can run for months unnoticed.
The extra development checks apply to the subtree you wrap and nothing above it, so it can be adopted incrementally. All of it is removed from production builds, so the double-invocation it causes is never a runtime cost for users.
S. Server rendering and static prerendering
Synchronous, no streaming, and it cannot wait for a Suspense boundary's data — it renders the fallback instead. It is the legacy path, kept for simple cases and for environments that cannot stream.
Produces HTML without the attributes hydration needs, for output that will never become interactive. Email templates are the usual case. Trying to hydrate its output does not work, which is the deliberate distinction from the previous entry.
The Node stream API, with a callback when the shell is ready and another when everything has resolved. Choosing between those two callbacks is the whole streaming decision, so they are the part worth understanding.
Respond at shell-ready to stream: the user gets the page frame immediately and the rest arrives as it resolves. Wait for all-ready when the consumer cannot stream — a crawler, or a build step writing a file — and needs the complete document.
The Web Streams variant, for edge runtimes, Deno and workers where Node streams do not exist. Same model as the Node version, different stream type, and the choice is dictated by the runtime rather than by preference.
The option that emits the script tags which start hydration. Omit it and the page renders as static HTML that never becomes interactive — which looks like a hydration failure and is actually a missing bundle.
A boundary's HTML is sent after the shell, once its data resolves, and React inserts it in place of the fallback. This happens before and independently of the client bundle loading, so content appears without waiting on JavaScript.
The static API waits for all the data and returns finished HTML, which is what a build-time generator wants. It is the all-ready behavior packaged as its own entry point rather than a callback.
A shell rendered at build time can be resumed at request time to fill in the parts that depend on the request. It splits one page between static and dynamic rather than forcing the whole route to be one or the other.
Effects never run during server rendering, so anything in one is absent from the HTML — and any browser API it touches is unavailable. Mount-only logic therefore belongs to the client phase by definition, which is the thing to plan around rather than work around.
T. Server Components and Server Functions
They run at build or request time and send their output, not their code. Their imports stay out of the client bundle too, which is why a heavy formatting or parsing library used only on the server costs the browser nothing.
A Server Component renders once and is gone, so there is nothing for state or an Effect to persist across, and no event loop to attach a handler to. Needing any of those is the signal that the component belongs on the client.
A Server Component may be declared async and await its data directly in the body. That removes the fetch-in-an-Effect dance entirely — no loading state, no race condition, no dependency array — because the data is resolved before the output exists.
The directive marks an entry point into the client bundle, not a single component. Everything the module imports comes with it, so placing the directive is a decision about where the boundary sits rather than an annotation on one component.
Because everything imported from a client module is also client code, a boundary placed high pulls a large subtree across with it. Boundaries grow quietly this way: one convenient import at the top of a file can move a great deal into the bundle.
Values crossing the server-to-client boundary have to survive serialization, which functions and class instances do not. A Server Function is the exception — it crosses as a reference the client can call, which is what makes passing behavior across possible at all.
A Client Component cannot import a Server Component, but it can receive one as children. The server renders it and passes the finished output through, which is how a client-side layout wraps server-rendered content.
The directive exposes a function the client is allowed to call, compiled into an endpoint. Calling it looks like an ordinary async function call, and that is precisely why the next point matters.
Because it compiles to a callable endpoint, anyone can invoke it with any arguments — not only your UI. Every one must authenticate, authorize and validate for itself. The convenience of calling it like a local function is what makes this easy to forget.
Wrapping a data call memoises it for one server render, so two components asking for the same record fetch once. It removes the need to hoist the fetch and thread the result down, which would otherwise be the only way to avoid the duplicate.
An abort signal tied to that cache's lifetime, so work started for a render that is subsequently discarded can be canceled rather than running to completion. It matters most where a render is abandoned often.
Server Components are a component model — where code runs and what ships. Server rendering is producing HTML from a tree. You can have either without the other, and conflating them is why the two feature sets get argued about as if they were one.
U. Error boundaries, class components and testing
There is no Hook equivalent, so catching a render error still requires a class implementing one of the two error lifecycles. Most codebases write one and reuse it, or take a library's, and never write another class for anything else.
They catch errors thrown while rendering, and nothing else: not event handlers, not async callbacks, not server rendering, and not errors thrown by the boundary itself. Assuming they are a general try/catch is the usual misunderstanding.
A boundary around one widget degrades that widget and leaves the page working; one boundary at the root turns any failure into a blank screen. Placement is a product decision about what the user should still be able to do when something breaks.
One returns the state that renders the fallback; the other is where logging belongs, because it is allowed to have side effects. Splitting them keeps the render path pure even when it is handling a failure.
A boundary stays in its error state until something clears it — changing its key, or resetting the state it holds. Without that there is no retry, and the user's only recourse is a full page reload.
The root accepts handlers for caught, uncaught and recovered errors, which puts reporting in one place instead of in every boundary. It is also the only way to see the recovered ones, which no boundary is told about.
The three class lifecycle methods collapse into a single Effect with a cleanup, because they were always describing one thing: start synchronizing, keep synchronizing, stop. Translating them one-for-one into separate Effects misses that.
A class extends Component and returns its UI from render(), so there is no top-level return and no Hook can be called anywhere inside it. Worth being able to read, because error boundaries and older code still use the form.
this.setState merges a partial object into the existing state, where the Hook setter replaces the value outright. Porting a class to Hooks without noticing that is how fields silently disappear from state during a migration.
A plain class method passed as a callback loses its receiver and this is undefined when it runs. A class arrow field or a bind in the constructor fixes it; the symptom is a TypeError from inside a method that looks correct.
A class cannot call Hooks, so it reads context through a static property or a consumer component wrapping its output. The static form handles one context only, which is part of why context-heavy code pushed people toward function components.
Labels a custom Hook's value in DevTools so the inspector shows something meaningful instead of raw internals. It only does anything inside a custom Hook, and it is purely a development aid.
Wrapping an update in act flushes the resulting renders and Effects before the next line, so assertions run against a committed UI. Without it a test can assert on the state between a trigger and its render and fail intermittently.
V. Client-side routing
React itself has no router; a library swaps which components render and updates the history entry. Because the document never reloads, everything held in memory survives the move — which is the feature, and also why a stale cache or a listener nobody removed now outlives the page it belonged to.
A router link cancels the browser's own navigation and pushes a history entry instead. A plain anchor to an internal route reloads the document, discarding in-memory state and re-downloading the bundle — it works, which is why the mistake survives review.
A dynamic segment turns part of the path into a value the component can read, which is why one definition covers every record rather than needing a route each. Treat that value as untrusted: it comes from the URL, so anything at all can be typed into it.
Navigating from code once something has finished — a save completed, a session expired — rather than from a click. It belongs in the handler or after the action resolves; triggering it from an Effect that watches state fires on every path to that state, including a restored one.
The parent declares where its children render, and they appear there — so the surrounding chrome stays mounted as you move between sibling routes. Scroll position and any state that layout holds therefore survive, which a flat list of routes rebuilding the page cannot offer.
Gating a route on authentication and redirecting when the check fails. Hiding the link is not the control — the URL is reachable by typing it — and the client-side guard is a convenience, not security: the server still has to authorize every request behind it.
Starting the fetch as the navigation begins rather than after the component mounts removes a waterfall: the request and the code download overlap instead of queuing. Fetching in an Effect guarantees the request cannot start until the component has already rendered once.
W. State and server-state libraries
Context re-renders every consumer whenever the value changes, with no way to subscribe to part of it. For state that changes often and is read by many components for different reasons, that is the wrong shape — and it is the specific problem selector-based stores exist to solve.
One store read through selectors, or many small independent atoms. Both avoid re-rendering readers that do not care, by opposite means: the first by narrowing what each reader subscribes to, the second by making the units small enough that subscription is automatic.
Describing an update as a serializable action applied by a pure function is what makes the history inspectable, replayable and testable. Nothing else about a store gives you those three; they follow from the shape, not from the library.
A component subscribes to the slice it reads, so an unrelated change to the store does not wake it. The selector has to return a stable value for that to work — one that builds a new object each call re-renders on every store change regardless.
Keeping the reducer pure means async work sits around the dispatch rather than inside it, as middleware or as a thunk. That preserves the properties above: the transition stays replayable because it never did anything but compute.
Data the server owns can change without you, so your copy is a cache and not state. Naming it that way makes the real questions obvious — how long a value stays fresh, when to revalidate, what invalidates it — and those questions have no answer when you call it state.
Loading, error, empty and data are four distinct screens, and empty is the one most often missed. Hand-writing all four in every component is the boilerplate a query library removes, which is most of the reason to adopt one.
A key names a cached request. It dedupes concurrent callers asking for the same thing, it is what a second component reads from instead of refetching, and it is the handle you invalidate once a mutation has changed the underlying data.
Serve what is cached immediately, refetch in the background, and swap the fresh value in when it lands. The screen is never blank on a repeat visit, and the cost is a moment where the user is looking at data that is being replaced.
A tab left open in the background goes stale without anything happening in it, so returning to it and regaining connectivity are the natural moments to revalidate. Both are cheap signals that catch the case a time-based policy alone handles badly.
How long a value counts as fresh and how long an unused entry is kept are two different windows. Confusing them gives you either constant refetching or reads that are older than you intended, and the two symptoms look nothing alike.
After a mutation you can patch the cached value directly, which is instant and can drift from what the server actually stored, or mark it stale and let the next read refetch, which is correct and costs a round trip. Pick per case rather than globally.
Apply the expected value, keep a snapshot, restore it if the write fails. The step people miss is canceling any read already in flight — otherwise a response that left before your write lands on top of it and silently undoes the optimistic change.
A read that needs another's result should be disabled until that input exists, rather than chained through an Effect. Gating keeps it declarative and keeps the loading states honest: a query that has not been allowed to start is not the same as one that is loading.
Keeping the previous page on screen while the next loads avoids a flash back to a spinner, and an infinite list accumulates pages rather than replacing them. They are different modes over the same cache, and mixing them up produces a list that keeps resetting.
Fetching before the component that needs it renders — on hover, or as a route transition begins — turns the eventual read into a cache hit. It is the same request either way; the only thing that changed is when it started.
When a refetch returns data that is mostly unchanged, reusing the references of the unchanged parts means memoised consumers do not see a change. Without it every background refetch re-renders everything, and the optimization upstream is wasted.
X. Testing React components
Assert on what a user can see and do. A test written against internal state or a component's private structure fails when you refactor without changing behavior, which teaches people to delete tests rather than trust them.
Find elements the way a user and a screen reader do: accessible role and name first, then label or visible text, with a test id as the last resort. A test that can only find an element by test id is also telling you that element has no accessible name.
A real interaction is a sequence — focus, key down, input, key up — and firing one synthetic event skips most of it. Code that depends on focus or on a key handler passes the synthetic test and fails for the user.
The find and wait helpers retry until the assertion passes or the timeout expires, so they settle as soon as the UI is ready. A fixed delay is both slower than that and flakier, because the right delay depends on the machine.
Exercises a custom Hook without hand-writing a host component around it. Useful for a Hook with real logic of its own; less so for one that only makes sense in the context of the component it was extracted from.
Intercepting at the network boundary leaves the component's own fetching, error handling and retry logic under test. Stubbing the module or seeding the state skips exactly the code most likely to be wrong.
Y. Accessibility in React
An input's accessible name comes from a label whose htmlFor matches the input's id. The id has to be unique on the page and stable across server and client, which is what the id-generating Hook is for — hand-written ids collide as soon as the component renders twice.
aria-* and data-* keep their hyphens in JSX, unlike every other attribute React camelCases. Writing ariaLabel produces an unknown prop that React passes through or warns about, and the element ends up with no accessible name.
A real button is focusable, activates on Enter and Space, and announces its role — all without any code. A div with a click handler has none of that, and rebuilding it correctly takes a tabindex, a role and two key handlers that are easy to get subtly wrong.
Moving focus deliberately — into a dialog that just opened, or onto the field that failed validation — is what keeps a keyboard or screen-reader user oriented. It has to happen after the commit, which is why it needs a ref and an Effect rather than a render-time call.
Swapping content without a document load means the browser never moves focus and a screen reader is told nothing happened. The arriving page has to claim focus itself, usually on its heading — otherwise the user is left pointing at a position on a page that is gone.
Focus moves into the dialog when it opens, cannot leave while it is open, and returns to whatever opened it on close. All three parts are required — returning focus is the one most often skipped, and its absence drops the keyboard user at the top of the document.
A live region announces a change the user did not cause, such as a result count updating or a background save completing. Politeness decides whether it waits for a pause or interrupts, and marking routine updates as assertive makes the page hostile to listen to.
A composite widget — a tab list, a menu, a grid — takes a single Tab stop and moves internally with arrow keys, so a keyboard user is not tabbing through fifty items. A skip link does the same job for repeated page navigation.
Z. Styling approaches
CSS Modules rewrite each class to something unique per file at build time, so styles cannot leak between components. It is a compile step with no runtime cost, and the trade-off is that styles cannot depend on props — they are static by construction.
Writing styles in JavaScript lets them depend on props and keeps them next to the component. The cost is paid at runtime, on every render that produces new styles, which is why the approach lost ground to build-time alternatives for high-traffic paths.
Composing small single-purpose classes in className rather than naming a class per element. The stylesheet stops growing with the app, and the markup carries the styling — which reads badly to some and removes an entire category of naming decisions.
Joining class names from conditions needs care with falsy values and separators, which is why a small helper is standard. Concatenating by hand produces stray spaces and the literal string false in a class list often enough to be worth avoiding.
Animating React is a mount and unmount problem. An entrance needs the element present before the transition starts, an exit needs the unmount delayed until it finishes, and a layout change needs a measurement or a frame callback between the two positions. All three are about timing, not CSS.
AA. Typing a React app
Props are one object, so they are typed by one interface or type alias on the parameter. This replaces runtime prop checking entirely: the error appears at the call site while writing the code, rather than as a console warning after the component has rendered.
ReactNode covers everything React can render — elements, strings, numbers, arrays, null — which is what children actually receives. Typing it as a single element rejects a string and a fragment, and is the usual cause of a caller having to wrap perfectly valid content.
React's synthetic events have their own generic types parameterized by the element, so a change handler on an input is not the DOM's own event type. Getting this wrong is why event.target.value so often fails to type-check.
The initial value determines the type, which is right until the value cannot express the full set — starting at null infers null, and starting at an empty array infers an array of nothing. Those are exactly the cases that need the type argument spelled out.
Two different uses with two different shapes. A DOM ref starts as null and stays nullable, because the node does not exist until the commit. A mutable value ref is initialized with a real value and is never null, so it needs no guard at every use.
A discriminated union of actions lets the reducer narrow inside each case and turns an unhandled action into a compile error. Typing the action loosely gives up both, and the reducer becomes the one place in the app where a typo is not caught.
A context with no sensible default is typed to include null and defaults to it, which makes using it without a provider a type error rather than a silent undefined. Put the guard in a custom hook that throws, so consumers get a value and not a maybe-value.
A type parameter on the component lets the item type flow from the data prop into the callbacks, so a render callback receives the real type instead of something unknown. It is what makes a reusable list or table typed for its caller rather than for its author.
A discriminant field makes mutually exclusive prop sets a compile error — pass the wrong combination and it does not build. Optional props cannot express that: they permit every combination, including the ones the component cannot handle.
Deriving a component's props from the component itself means a wrapper cannot drift from what it wraps. Add a prop to the inner component and the wrapper's type follows automatically, rather than being a copy someone has to remember to update.
Return an array and its type widens to an array of whatever union its members form, so every destructured position gets the same over-broad type. A const assertion, or an explicit tuple, pins each slot — which is what lets a custom Hook destructure as cleanly as the built-in ones.
It annotates the function rather than the props, which once implied an implicit children prop and still gets in the way of generic components. Typing the props parameter directly is simpler and does not constrain what the component can be.
AB. The Rules of React, enforced — eslint-plugin-react-hooks
The Rules of React are not checked at runtime; a lint plugin checks them at build time. That is the difference between seeing a violation in the editor while writing it and debugging its consequence in production weeks later.
The plugin reports the Compiler's diagnostics even in a project that has not adopted it, so the rules can be read as a checklist before any build change is made. Clearing them first is what makes the eventual adoption uneventful.
When the Compiler reports a violation it stops optimizing that component or Hook and carries on with the rest. Adoption is therefore incremental by construction: a violation list is a list of missed optimizations, not a blocker to be cleared first.
They are separate diagnostics because they are separate mistakes. Setting state during render risks looping the render; setting it synchronously inside an Effect cascades an extra render pass. Reading them as one rule leads to fixing the wrong half.
Hand-written memo, useMemo and useCallback are promises the code already makes, and compilation must not break them. The rule fires where the Compiler cannot preserve one — which usually means the surrounding code is doing something it cannot analyze.
A component defined inside another is a new type on every render, so React remounts its subtree rather than updating it. It is the same defect the state chapter describes, caught statically instead of discovered as a mysterious loss of state.
Four rules enforcing one idea between them: render must not mutate globals, props, state or a ref, and must not call something known to be impure. They are separate diagnostics because they catch the idea at four different sites.
The reference page enumerates the recommended preset, and the installed plugin exports more rules than that. So a rule missing from the documented list is not necessarily unavailable — check what the plugin actually exports before concluding it does not exist.
Keentune is not affiliated with or endorsed by the organizations whose documentation informs these maps.
All exam, test, and product names and trademarks are the property of their respective owners and are used here for identification and reference only. Keentune is independent study practice — not affiliated with, authorized, or endorsed by any of these organizations.