Adaptive skill practice — find your level, get sharper.
React practice
The mental model behind hooks and re-renders
The React model senior frontend interviews probe — components and props/state, the hooks (useState, useEffect, useMemo, useCallback, useRef, useReducer) and the rules that govern them, reconciliation and why keys matter, what actually triggers a re-render, performance (memo, lifting state, Suspense, transitions), and the classic gotchas (stale closures, index keys, direct mutation). Core hooks are version-stable; React 18/19 features are flagged. Part of the Developer track.
• Meets you at your level — never too easy, never too hard.
• Adapts every question to how you’re doing.
• Tracks your level over time. Skill practice, not a test.
What you’ll master
Follow the curriculum from foundations through advanced practice. Each line is a concept the adaptive question bank can teach and test.
A. JSX and describing the UI
•
a component is a function returning markup, and the capital letter is what distinguishes it from a DOM tag
•
a component returns one node; <>…</> groups siblings without emitting a DOM element
•
JSX requires <img /> and <li></li>; unclosed tags are a syntax error, unlike HTML
•
className, htmlFor, tabIndex — attribute names are JavaScript property names, not HTML ones
•
{} interpolates an expression; if, for and other statements cannot go inside
•
style={{color:'red'}} is an object literal inside the expression slot, not special syntax
•
markup compiles to a plain element object, so it can be stored in a variable, held in an array, or returned from a branch
•
nested markup arrives as props.children, which is how a layout or card component takes arbitrary content
•
a component that returns null still ran and its Hooks still executed; only its output is empty
•
{count && <Badge/>} prints a literal 0 when count is 0; test explicitly or use a ternary
•
interpolated strings are escaped, so {userInput} cannot inject markup; dangerouslySetInnerHTML is the deliberate opt-out
•
one default export per file, any number of named ones, and the import syntax differs for each
B. Props and composition
•
a component may never write to its props; only the owning parent can change them
•
a destructuring default fires for undefined only, not for null, 0, or ''
•
{...props} forwards everything and makes it impossible to see which props a component actually uses
•
accepting children beats threading a prop through intermediate components that do not care about it
•
passing a function as a prop lets the child decide when, how often, and with what data to call it
•
re-rendering a parent re-renders its children by default, even when their props are unchanged
•
a key identifies a list item across renders and must be stable and unique among its siblings
•
using the array index as a key mismatches state to items after an insert, delete, or reorder
•
key is consumed by React and never appears in the child's props
•
a higher-order component takes a component and returns a wrapped one, which adds a layer to the tree that a custom Hook does not
•
a parent and its children share implicit state through context, so the consumer composes them freely instead of configuring one component through props
•
headless behaviour, a consumer-supplied state reducer and prop getters all hand a decision back to the caller instead of fixing it inside the component
•
an as prop lets one component render as different underlying elements while keeping its behaviour and its accessibility
C. State: a component's memory
•
the current value and a setter, destructured by position, one call per state variable
•
two elements of the same component hold entirely independent state
•
the argument is used on the first render only; changing it later does not reset the state
•
useState(createBoard) calls the function once; useState(createBoard()) calls it on every render
•
a setter does not assign to the variable; it requests another render with a new value
•
the value is frozen for the whole render, so setC(c+1) three times still adds one
•
setC(c => c + 1) queues a transformation against the pending value and composes correctly
•
every update in the same tick is batched into a single re-render, including in timeouts and promises since 18
•
setting state to an Object.is-equal value skips the re-render of the children
•
calling the setter for the *same* component while rendering is the sanctioned way to adjust state on a prop change
•
the nested definition is a new type each render, so its subtree remounts and loses state
•
the same component at the same position keeps state; move it and the state is destroyed
D. Render, commit, and reconciliation
•
a mount or a state update triggers a render, React calls the component, then commits the DOM changes
•
untouched nodes are left alone, which is why scroll position, focus, and video playback survive a re-render
•
the browser paints after the commit, which is exactly the window useLayoutEffect runs in
•
a re-render that produces identical output performs no DOM work; "re-render" is not "repaint"
•
the same type at the same position updates in place; a different type unmounts the subtree and destroys its state
•
moving a component under a different parent element resets its state even though the JSX looks the same
•
giving a component a different key is the deliberate way to reset all of its state
•
same props and state must produce the same JSX, with no mutation of anything created before this render
•
development calls components twice to surface impurity; production renders once
E. Updating objects and arrays immutably
•
mutating an object already in state triggers no render and leaves the UI showing stale data
•
spread into a new object and set that; the spread is shallow and shares nested references
•
changing a deep field means copying every object on the path down to it
•
push, pop, splice, sort and reverse mutate; concat, slice, map, filter and toSorted return new arrays
•
filter removes by predicate, and spreading two slices inserts at an index
•
return a copy for the matched item and the original object for every other
•
memo, dependency arrays and store selectors compare by reference, so an in-place mutation is invisible to all of them
•
a producer (Immer) lets you write mutating syntax against a draft while still yielding a new object
F. Choosing, lifting and sharing state
•
two values that always change together belong in one state variable
•
two booleans that can both be true encode an impossible UI; a single status string cannot
•
anything derivable from props or other state should be computed in render, not stored
•
store the selected id, not a second copy of the selected object that can drift
•
normalize to a map of ids so an update touches one node instead of rebuilding a tree
•
a plain const recomputed each render is always fresh; an Effect that writes derived state renders twice and can be stale
•
move shared state to the closest common ancestor and pass the value plus a handler down
•
a component driven entirely by props is controlled; one holding its own state is uncontrolled
•
value without onChange freezes the field; defaultValue is the uncontrolled spelling
•
state held higher than its only consumer re-renders the whole subtree for nothing
G. Reducers
•
(state, action) => nextState, and React calls it with the action you dispatched
•
name an action after what the user did (added_task), not after which field to overwrite
•
no fetching, no timers, no mutation; StrictMode may call it twice to prove it
•
dispatch never changes between renders, so it is safe to omit from dependency arrays
•
a reducer centralizes many related transitions and makes them testable without rendering
•
the third argument computes the initial state lazily and gives you a reusable reset
•
the next state is unavailable until the next render, so you cannot read it after dispatching
H. Context
•
the createContext default applies when no provider is above, not when a provider passes undefined
•
React 19 renders <Context value={…}> directly; <Context.Provider> still works but is the older spelling
•
a nested provider shadows an outer one for its own subtree only
•
an inline object as the value is new every parent render and re-renders every consumer
•
every consumer re-renders on any change to the value; there is no selector or partial subscription
•
putting the stable dispatch in its own context keeps action-only consumers from re-rendering
•
a portal moves the DOM node, not the React tree position, so providers above still reach it
•
the built-in state-management pattern: one reducer, a state context, and a dispatch context
•
passing children or one extra prop is usually simpler than introducing a context
I. Refs and the DOM
•
writing ref.current changes the value without scheduling a render
•
reading or writing a ref while rendering breaks purity; do it in an event handler or an Effect
•
refs hold things the UI does not display: timer ids, previous values, DOM nodes, instance scratch space
•
putting ref on a host element gives you the real node once React has committed it
•
the node does not exist while rendering, only after the commit, so guard the access
•
function components receive ref as an ordinary prop in React 19
•
forwardRef existed because ref used to be stripped from props; new code does not need it
•
a function ref receives the node on attach, and in React 19 its returned function is the detach cleanup
•
exposes a narrow, named imperative API to a parent instead of the raw DOM node
•
manually mutating nodes React manages will be undone or will corrupt the tree; only touch nodes React never updates
J. Effects: synchronizing with external systems
•
an Effect connects React to something outside it (a socket, a widget, the browser API), never to compute a value
•
the user sees the committed render first, then the Effect fires
•
omitted array runs after every commit, [] runs on mount, [a] runs when a changes by Object.is
•
the previous Effect's cleanup runs before the next one and again on unmount
•
development mounts, unmounts and remounts, so a missing cleanup shows itself immediately
•
think "start syncing / stop syncing", not "on mount / on update"; that framing produces the right cleanup
•
an ignore flag or an AbortController in the cleanup stops a slow stale response from overwriting a fresh one
•
a value created during render is a new dependency every render; move it inside the Effect or memoize it
•
you cannot silence the linter safely; change the code until the dependency is genuinely unnecessary
•
reading state via the functional updater lets you drop that state from the array
•
an Effect Event reads the latest props and state without making the Effect re-run, and is never a dependency
•
synchronous measurement before the browser paints, at the cost of blocking it
•
injects style rules before any layout Effect reads layout, and cannot read refs
K. You might not need an Effect
•
computing during render beats an Effect that sets state, which costs an extra render pass
•
cache the calculation, do not move it into an Effect
•
reset all of a component's state on a prop change with a key, not with an Effect that clears every field
•
a partial reset on a prop change is a setter called during render, not an Effect
•
a request that happens *because the user clicked* goes in the click handler, not in an Effect watching state
•
an Effect setting state that wakes another Effect should be one calculation or one event handler
•
once-per-app work belongs at module scope or behind a guard, not in a mount Effect that StrictMode runs twice
•
call the parent's callback in the event, instead of notifying the parent from an Effect
L. Rules of React and custom Hooks
•
never inside a condition, loop, or nested function; call ORDER is how React matches a Hook to its state
•
components and custom Hooks only, never a plain helper, a class method, or an event handler
•
a function is a Hook because its name starts with use; the linter and the Compiler key off that name
•
two components calling the same custom Hook get completely separate state
•
same inputs, same output, and no mutation of anything outside the render's own scope
•
no network calls, no DOM writes, no subscriptions, no logging to a shared mutable object while rendering
•
including a value already handed to JSX, because React may reuse it
•
render <Foo/>; calling Foo() collapses it into the caller and destroys its state and Hooks
•
use may be called inside conditionals and loops, unlike every other Hook
•
the rules are enforceable lint; suppressing them is how stale-closure bugs reach production
M. Performance, memoization and the Compiler
•
it skips a re-render only when every prop is Object.is-equal to last time
•
a fresh object, array or arrow function prop makes memo useless on every render
•
it recomputes only when a dependency changes, and is a hint React may discard, not a guarantee
•
useCallback(fn, deps) is exactly useMemo(() => fn, deps)
•
every memo costs a comparison and retained memory; measure before adding one
•
the cache can be thrown away, so anything that must happen exactly once cannot live there
•
content passed down as children keeps the same element object, so React skips it without any memo
•
the React Compiler inserts the memoization for you, making most hand-written memo/useMemo redundant
•
it optimizes only code obeying the Rules of React and bails out of anything it cannot prove safe
•
'use memo' and 'use no memo' opt a function in or out when the Compiler runs in annotation mode
•
React's own tracks in a browser profile split its work by SCHEDULING PRIORITY (blocking vs transition) and show when an update was scheduled against when it rendered
•
<Profiler onRender> reports the actual and baseline render duration of a subtree, in development builds
•
rendering only the rows currently in view plus a small buffer, so a long list costs what a screenful costs
N. Transitions, deferred values and external stores
•
an update inside startTransition is non-urgent and can be abandoned when an urgent update (a keystroke) arrives
•
useTransition returns a pending flag scoped to that component, for showing a spinner without blocking input
•
the non-Hook form works outside components but gives you no pending flag
•
React 19 lets the transition function be async, so pending state spans the await; this is what an Action is
•
a suspending update inside a transition keeps the current UI on screen instead of flashing the Suspense fallback
•
renders with the previous value first, then re-renders with the new one in the background
•
the second argument supplies the value to use on the very first render, before anything to defer exists
•
you defer a value you were handed; you transition an update you trigger
•
concurrent rendering can read an external mutable store twice and get two different values in one tree
•
subscribe plus getSnapshot is the tear-free way to read a non-React store
•
returning a new object each call makes React re-render forever; cache the snapshot
•
the third argument supplies the value used during server rendering and hydration
•
<Activity> keeps a subtree mounted with its state while deprioritizing it, so a hidden tab can pre-render instead of being destroyed
O. Suspense, lazy and the use Hook
•
<Suspense fallback> displays the fallback while anything in its subtree is suspended
•
lazy components, use on a promise, and framework-integrated data; a plain fetch inside an Effect never suspends
•
calling lazy() inside a component creates a new type each render and remounts the subtree
•
siblings inside the boundary vanish too, so where you place the boundary is a UX decision
•
an inner boundary lets the fast part of a page appear while a slower part still loads
•
use(promise) suspends the component and returns the resolved value on the retry
•
a promise built during render is new on every attempt; it must come from a cache or a Server Component
•
use(Context) behaves like useContext but may be called conditionally
•
the server streams a boundary's HTML when it resolves and React swaps it in place of the fallback
•
Suspense handles the pending state only; a rejected promise must be caught by an error boundary
P. Forms, Actions and optimistic UI
•
passing a function to <form action> calls it with the FormData on submit and resets uncontrolled fields afterwards
•
an Action is managed by React, needs no preventDefault, and can work before hydration finishes
•
[state, formAction, isPending], wrapping an action so its result becomes state
•
under useActionState the action signature is (previousState, formData), with the form data second
•
it reads the status of a parent <form>, so calling it in the component that renders the form returns nothing pending
•
shows the expected result immediately and reverts automatically when the action settles or fails
•
the optimistic value only exists while the action runs; the real state takes over afterwards
•
an input without a name attribute never appears in the submitted FormData
•
switching value from a string to undefined turns a controlled input uncontrolled and React warns
•
React sets the option through value on <select> and <textarea>, not via selected or inner text
•
a checkbox or radio is controlled by checked plus onChange, not by value
•
validating the whole form against one schema, and choosing when that runs relative to the user's typing, blurring and submitting
•
rows a user can add and remove need a stable identity per row, exactly as any other list does
Q. react-dom: events, portals and resources
•
React wraps the browser event in a normalized object, with the original available as nativeEvent
•
listeners live on the root container, not each node, which is why mixing React with manual listeners can surprise you
•
onClickCapture runs on the way down, before the bubbling handlers
•
one cancels the browser's default behavior, the other stops the walk up the tree
•
onClick={handleClick} registers it; onClick={handleClick()} runs it during render
•
style takes a camelCased object, and a bare number becomes pixels for most properties
•
createPortal moves the DOM node while leaving the component where it is in the React tree
•
an event from a portal bubbles to the React parent, not to the portal's DOM parent
•
the escape hatch for reading the DOM immediately after an update; it defeats batching and hurts performance
•
React 19 hoists <title>, <meta> and <link> rendered anywhere in the tree into <head>
•
a precedence attribute on a stylesheet link lets React order and deduplicate stylesheets across components
•
preload only downloads the resource, preinit downloads and evaluates it
R. Client roots, hydration and StrictMode
•
a root is created for a DOM container, and root.render(<App/>) mounts the tree into it
•
calling render on an existing root updates the tree rather than remounting it
•
root.unmount() tears the tree down and runs every cleanup; the root cannot be used again afterwards
•
hydration reuses server-rendered markup instead of recreating it, and the first client render must match
•
Date.now(), Math.random(), locale formatting and window checks all produce mismatches
•
a one-level opt-out for genuinely unavoidable differences such as a rendered timestamp
•
React 19 logs a single diff and re-renders the affected subtree on the client instead of silently patching
•
generates an id stable across server and client for htmlFor/aria-*; it is never a valid list key
•
the root option that reports errors React recovered from, such as a hydration re-render
•
StrictMode's extra checks apply to the wrapped subtree only and are stripped from production builds
S. Server rendering and static prerendering
•
synchronous, no streaming, and it cannot wait for Suspense data; the legacy path
•
HTML without hydration attributes, for output that will never become interactive, such as email
•
the Node stream API, with onShellReady and onAllReady callbacks
•
respond at onShellReady to stream, wait for onAllReady for crawlers and static generation
•
the Web Streams variant for edge runtimes, Deno and modern workers
•
the option that emits the script tags which start hydration on the client
•
a boundary's HTML arrives after the shell and is inserted without waiting for the client bundle
•
react-dom/staticprerender waits for all data and returns HTML for build-time generation
•
a prerendered shell can be resumed at request time to fill in the dynamic parts
•
Effects never run during server rendering, so mount-only logic and browser APIs are unavailable there
T. Server Components and Server Functions
•
they render at build or request time, and their code and dependencies stay out of the client bundle
•
a Server Component cannot use useState, useEffect, or attach event handlers
•
a Server Component may be an async function that awaits its data directly, with no fetching Effect
•
the directive names an entry point into the client bundle, not a single component
•
everything imported by a 'use client' module is client code too, which is how a boundary quietly grows
•
values crossing from server to client must serialize; functions and class instances cannot, Server Functions can
•
a Server Component can be passed as children to a Client Component even though it cannot be imported by one
•
the directive exposes a function the client can call, and it is compiled into an RPC endpoint
•
every Server Function must authenticate, authorize and validate its own arguments
•
cache() memoizes a data call for the lifetime of a single server render, so two components asking for the same thing fetch once
•
an abort signal tied to that cache lifetime, for cancelling work when the render is discarded
•
Server Components are a component model; server-side rendering is an HTML-generation step, and either can exist without the other
U. Error boundaries, class components and testing
•
only a class implementing getDerivedStateFromError or componentDidCatch can catch a rendering error
•
event handlers, async callbacks, server rendering, and errors thrown by the boundary itself are not caught
•
a boundary around one widget degrades a section instead of blanking the whole page
•
getDerivedStateFromError renders the fallback, componentDidCatch is where logging side effects belong
•
changing the boundary's key or clearing its error state is what lets the user retry
•
createRoot accepts handlers for caught, uncaught and recovered errors so reporting is centralized
•
componentDidMount, componentDidUpdate and componentWillUnmount collapse into one Effect with a cleanup
•
a class extends Component and returns its UI from a render() method; there is no top-level return and no Hook may be called inside it
•
this.setState is asynchronous and MERGES the partial object into this.state, where useState's setter replaces the whole value
•
a plain class method loses its this when passed as a callback; a class arrow field or a bind in the constructor is what keeps it
•
a class reads context through static contextType or a Context.Consumer, because Hooks cannot be called from a class
•
labels a custom Hook's value in React DevTools, and is only meaningful inside a custom Hook
•
wrapping updates in act flushes renders and Effects so assertions run against the committed UI
V. Client-side routing
•
React ships no router, so navigation is a library swapping components and updating history: the document never reloads, and app state survives the move
•
a router link cancels the browser's default navigation and pushes history; a bare <a> reloads the document and discards everything held in memory
•
a dynamic segment captures part of the URL as a value the matched component reads, so one route definition serves every id
•
navigating from code once an action has completed, rather than from a user's click on a link
•
a child route renders inside its parent's layout through an outlet, so shared chrome is not torn down and rebuilt per route
•
gating a route on auth state and redirecting; hiding the link is not the control, because the URL stays reachable by hand
•
starting a route's fetch as the navigation begins, instead of waterfalling from an Effect inside the component after it mounts
W. State and server-state libraries
•
every context consumer re-renders on any change to the value, so widely-shared state with many independent readers wants selector subscriptions instead
•
one centralized store read through selectors, versus many small independent atoms; both avoid re-rendering unrelated readers, by opposite means
•
describing an update as a plain serializable action applied by a pure reducer is what makes a store's history inspectable, replayable and testable
•
subscribing a component to the slice it actually reads, so an unrelated change to the store does not re-render it
•
the reducer stays pure, so async work sits in middleware around the dispatch rather than inside the update
•
data a server owns can change without you, so locally it is a cache and not state, which makes freshness and invalidation first-class concerns
•
loading, error, empty and data are four distinct states a screen renders, and hand-rolling them per component is the boilerplate a query library removes
•
a key identifies a cached request, dedupes concurrent callers of it, and is what you invalidate once a mutation has changed the underlying data
•
serve the cached value immediately, refetch in the background, and swap the fresh one in when it arrives
•
a tab left open goes stale, so returning to it and regaining connectivity are the moments worth revalidating on
•
how long a value counts as fresh is a different window from how long an unused entry is kept before eviction, and confusing them produces either stale reads or constant refetching
•
patching the cached value directly versus marking it stale and letting the next read refetch it
•
apply the expected value, keep a snapshot, restore it on failure, and cancel any in-flight read that could land on top of it
•
a read that needs another's result is DISABLED until its input exists, rather than chained through an Effect
•
keeping the previous page visible while the next loads, and accumulating pages for a load-more list
•
fetching before the component that needs it renders, so the read is a hit rather than a request
•
reusing the references of unchanged parts of a refetched result, so memoized consumers are not re-rendered by identical data
X. Testing React components
•
assert on what a user can perceive and do, so a refactor that preserves behaviour does not break the test
•
find elements the way a user and assistive technology do: accessible role and name first, label and text next, testID last
•
a real interaction is a sequence (focus, keydown, input, keyup), and firing one synthetic event skips most of it
•
findBy and waitFor retry until the assertion passes or times out, which is why a fixed delay is both slower and flakier
•
exercising a custom Hook without hand-writing a host component around it
•
intercepting at the network boundary keeps the component's own fetching code under test, where stubbing state skips it entirely
Y. Accessibility in React
•
an input's accessible name comes from a <label htmlFor> matching its id, and useId is what makes that id safe under server rendering
•
aria-* and data-* keep their HTML spelling in JSX, unlike every other attribute React camelCases
•
a real <button> arrives focusable, keyboard-activatable and announced by role; a div with onClick has none of that until you rebuild it
•
moving focus deliberately, into a dialog that just opened or onto the field that failed validation
•
a client-side navigation changes the page without a document load, so focus and the screen reader have to be moved by hand
•
focus moves into a dialog on open, cannot leave while it is open, and returns to whatever opened it on close
•
a region that announces a content change the user did not cause, with politeness deciding whether it interrupts
•
a composite widget takes one Tab stop and moves internally with arrows, and a skip link lets a keyboard user bypass repeated navigation
Z. Styling approaches
•
CSS Modules hash a class per file so styles cannot leak, which is a build-time transform rather than a runtime one
•
component-scoped CSS written in JavaScript, with styles that can be derived from props, at a runtime cost
•
composing many small single-purpose classes in className instead of naming a class per element
•
joining class names from conditions with a helper, which handles falsy values and spacing that manual concatenation gets wrong
•
animating a React tree is a mount/unmount problem: a transition needs the element to still exist, an exit needs the unmount delayed, and a layout change needs measurement or a frame callback
AA. Typing a React app
•
props are one object, typed by an interface or type alias on the parameter, which replaces the runtime PropTypes checks entirely
•
ReactNode covers everything renderable including strings, arrays, null and numbers, where JSX.Element accepts only a single element
•
React's synthetic events are their own generic types (ChangeEvent<HTMLInputElement>, MouseEvent<HTMLButtonElement>), not the DOM's Event
•
the initial value infers the type, so an explicit parameter is needed exactly when that value cannot express the full set (useState<string | null>(null))
•
a DOM ref is useRef<HTMLDivElement>(null) and is nullable until attached; a mutable value ref is useRef<T>(initial) and never null
•
a discriminated union of action types is what lets the reducer narrow per case and makes an unhandled action a compile error
•
a context with no sensible default is typed T | null with null as the default, and the guard belongs in a custom hook that throws rather than in every consumer
•
a type parameter declared on the component flows from the data prop into its callbacks, so a render callback receives the item type instead of unknown
•
a discriminant field makes mutually exclusive prop sets a compile error, which optional props cannot express
•
ComponentProps<typeof X> derives an existing component's props so a wrapper cannot drift from what it wraps
•
a returned array widens to a union array, so a custom hook needs as const or an explicit tuple type for destructuring to keep each position's type
•
React.FC annotates the function rather than the props, which once implied a children prop and still constrains generic components
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.