Keentune
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/static prerender 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.
Explore all skills on Keentune
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.
© 2026 SportaApp LLC