React 19 with function components: rendering and reconciliation, state, effects, forms and Actions, memoization, Suspense, Server Components and traps.
AMCompiled by Aditya Mishra · Technical Lead, BNP Paribas
The React facts interviewers keep coming back to, for React 19 and function components; every hook in detail lives in the React Hooks sheet.
Mental model & JSX
UI = f(state): a component maps props and state to JSX. You declare what the screen should show; React works out the DOM operations.
Rendering must be pure: same inputs, same JSX, no side effects. React may call a component more than once, pause it or discard the result.
Side effects belong in event handlers first, and in effects only to sync with something outside React.
JSX compiles to jsx() calls (the automatic runtime, required since React 19). Capitalized tags are components; lowercase tags are DOM elements.
JSX rule
Example
One root per return
wrap siblings in <>…</>
Close every tag
<img src={src} alt="" />
camelCase props
className, htmlFor, onClick; aria-* and data-* keep dashes
style is an object
style={{ fontSize: 14 }}
{} takes expressions
ternary, &&, .map(); no if/for inside
Renders nothing
null, undefined, true, false (but 0 and NaN do render)
Text is escaped
{userInput} can’t inject HTML
Components, props & state
Props are read-only inputs: function Avatar({ user, size = 40 }). React 19 ignores propTypes and drops defaultProps for function components (use default parameters).
children is what sits between the tags; slot props (<Layout sidebar={<Nav />}>) generalize it. Children talk to parents through callbacks (onSelect); siblings share state by lifting it up to their closest common parent.
key isn’t a prop (the child can’t read it). refis a plain prop in React 19, so forwardRef is no longer needed.
State is a per-instance snapshot: the setter schedules a re-render, and the variable keeps its value for the rest of the current render.
Automatic batching (React 18+): updates in one event, timeout or promise callback cause a single re-render (flushSync from react-dom opts out).
Use the updater form when the next value depends on the previous one; useState(() => build()) runs the initializer once.
An Object.is-equal value bails out. Update immutably ({ ...user, name }, [...items, item], filter, map): mutating and passing the same reference doesn’t re-render.
Derive what you can during render (full names, filtered lists), and don’t copy props into state: useState(props.x) reads x only once.
function Counter() { const [n, setN] = useState(0); function handleClick() { setN(n + 1); // replace with 0 + 1 setN(n + 1); // replace with 0 + 1 again setN((c) => c + 1); // queued value (1) + 1 } // one re-render, n becomes 2; inside the handler n is still 0 return <button onClick={handleClick}>{n}</button>;}
JavaScript
Rendering & reconciliation
Trigger → render (React calls components and diffs the output) → commit (applies only the DOM changes, attaches refs, runs layout effects) → browser paint → useEffect callbacks.
A component re-renders when its state changes, its parent re-renders (props changed or not) or a context it reads changes. New props are a consequence of the parent rendering, not a separate trigger.
Re-rendering isn’t a DOM update: identical output leaves the DOM untouched.
Reconciliation: a different element type at the same position unmounts the old subtree (state lost); the same type keeps its state and gets new props; list children are matched by key (keys note).
State belongs to a position in the tree. <Form key={userId} /> gives each user a fresh instance: the clean way to reset state.
Keys must be stable and unique among siblings (data ids). key={Math.random()} remounts everything on every render.
Index-key bug: prepend, sort or filter a list keyed by index and the state (input text, checkbox, focus) stays at its position while the data moves, so it appears on the wrong item. Index keys are safe only for static lists.
Fiber (React 16+) splits rendering into units of work, which lets React 18+ (createRoot) interrupt a non-urgent render for something urgent.
Hooks at a glance
Hook
Use it for
useState / useReducer
local state / many actions in a pure reducer
useContext / use
read context; use also reads a promise and may be called conditionally
useRef
a mutable box or DOM node that doesn’t re-render
useEffect / useLayoutEffect
sync with outside systems / measure the DOM before paint
useMemo / useCallback
cache a value / a function between renders
useTransition / useDeferredValue
mark an update non-urgent / render a lagging copy
useActionState / useOptimistic
Action result + pending flag / temporary optimistic UI
useFormStatus (react-dom)
pending state of the parent <form>
useId
ids for htmlFor and aria-* (not keys)
useSyncExternalStore
subscribe to a store outside React
useEffectEvent (19.2)
read the latest values in an effect without adding deps
Rules of Hooks: call them only at the top level of components and custom hooks; never in conditions, loops, nested functions or after an early return. React matches hook state by call order.
A custom hook (useSomething) shares logic, not state: each call has its own. Lint with eslint-plugin-react-hooks.
Effects & refs
An effect synchronizes with an external system: network, subscriptions, timers, browser APIs, non-React widgets.
Deps: none → after every render; [] → after mount; [a, b] → when either changes (Object.is). List every reactive value the effect reads.
Cleanup runs before the next run and on unmount. The setup can’t be async: declare an async function inside and call it.
Effects run only on the client, after commit; when not caused by an interaction, React generally lets the browser paint first. useLayoutEffect runs before paint (and blocks it).
useEffect(() => { const controller = new AbortController(); fetch(`/api/users/${id}`, { signal: controller.signal }) .then((res) => res.json()) .then(setUser) .catch((err) => { if (err.name !== 'AbortError') setError(err); }); return () => controller.abort(); // a stale response can't overwrite a newer one}, [id]);
JavaScript
You might not need an effect to…
Instead
compute a value from props/state
calculate it during render (useMemo if slow)
reset state when a prop changes
render with a key
react to a click
do it in the event handler
notify the parent
call its callback in the same handler
subscribe to a store
useSyncExternalStore
fetch page data
framework loaders, Server Components, TanStack Query
Refs: useRef(null) returns { current }, which persists across renders; changing it never re-renders. Use it for DOM access (ref.current.focus()), timer ids, previous values.
Don’t read or write ref.current during render; DOM refs are null until commit. React 19 ref callbacks may return a cleanup.
Forms & Actions
Controlled
Uncontrolled
Value lives in
state (value + onChange)
the DOM (defaultValue, defaultChecked)
Read it
any time, every keystroke
on submit, via FormData or a ref
Good for
live validation, formatting, dependent fields
simple forms; file inputs are always uncontrolled
value without onChange gives a read-only field. A value going from undefined to a string flips uncontrolled to controlled (warning): start with ''.
Form Actions (19): <form action={fn}> calls fn(formData) inside a Transition, always as POST, and resets uncontrolled fields after success.
useActionState(action, initial) returns [state, formAction, isPending]; the action gets (prevState, formData). useFormStatus() works only in a component rendered inside the form.
With Server Components, action can be a Server Function ('use server'), so the form can work before JavaScript loads.
createContext(defaultValue), provide with <ThemeContext value={v}> (19; .Provider still works), read with useContext or use. The default applies only when no provider is above.
Every consumer re-renders when the value changes (Object.is), even inside memo. A provider value like { user, setUser } is a new object each render: useMemo it, split state and dispatch into two contexts, or move the provider down.
Context is dependency injection, not a state manager: fine for theme, locale, current user; poor for fast-changing values.
Tool
Caches
Pays off when
memo(Comp)
render output; skips if every prop is Object.is-equal
a slow child re-renders with the same props
useMemo(fn, deps)
a computed value
the work is expensive, or the object feeds a memo child or deps
useCallback(fn, deps)
a function
it feeds a memo child or a dependency array
Wasted if the child isn’t memoized or another prop (inline object, children JSX) changes every render; a memo child still re-renders for its own state and context (memo note).
Cheaper fixes first: move state down, or lift content up by passing JSX as children.
React Compiler (v1.0 stable since October 2025): a build-time plugin that memoizes automatically when code follows the Rules of React, making most manual memoization unnecessary.
Suspense, errors & portals
<Suspense fallback={…}> shows the fallback while a child suspends: a lazy component, use(promise), or a Suspense-enabled framework or library. Fetching in an effect doesn’t suspend.
lazy(() => import('./Settings.js')): declare at module top level; the module must default-export a component. Split by route first.
A Transition keeps visible content instead of flashing a fallback; nested boundaries reveal progressively, and streaming SSR sends HTML per boundary.
Error boundaries are still classes (static getDerivedStateFromError for the fallback, componentDidCatch to log), or use react-error-boundary. Change the key to reset one.
They miss errors in event handlers, async code (setTimeout), SSR and the boundary itself; errors thrown in a startTransition function are caught. React 19 adds onCaughtError/onUncaughtError to createRoot.
Portals: createPortal(children, domNode) (react-dom) renders elsewhere in the DOM (modals, tooltips) but stays in the React tree: context works and events bubble to React ancestors.
Fragments: lists need <Fragment key={id}>, as <> takes no key. React 19.3 adds <Fragment ref>.
StrictMode (development only): renders twice, adds a setup+cleanup cycle to effects on mount, re-runs ref callbacks and flags deprecated APIs. An effect “running twice” exposes a missing cleanup.
Concurrent & Server Components
const [isPending, startTransition] = useTransition(): startTransition(() => setTab('posts')) makes the update interruptible, so typing stays responsive. Since 19 the function may be async (an Action); updates after an await need another startTransition. Not for controlled text inputs.
useDeferredValue(value) renders a lagging copy, for when you don’t own the setter. Unlike debouncing there’s no fixed delay and the background render is interruptible.
<Activity mode="hidden"> (19.2) hides a subtree but keeps its state; its effects are cleaned up until it’s shown again.
function Search({ items }) { const [query, setQuery] = useState(''); const deferred = useDeferredValue(query); return ( <> <input value={query} onChange={(e) => setQuery(e.target.value)} /> {/* SlowList is memo()ized, so it only re-renders when `deferred` changes */} <SlowList items={items} query={deferred} stale={query !== deferred} /> </> );}
JavaScript
Server Components (in an RSC framework such as the Next.js App Router) run only on the server or at build time: they can be async and read the database directly, add no client JavaScript, and can’t use state, effects or event handlers.
'use client' at the top of a file marks a module boundary: that module and all its imports become client code. Client Components are still server-rendered to HTML, then hydrated.
Server-to-client props must be serializable (primitives, plain objects, arrays, Date, Map, promises, JSX, Server Functions); no plain functions or class instances. Pass Server Components into Client Components as children.
'use server' marks Server Functions that the client can call, e.g. as a form action.
SSR vs RSC: SSR renders HTML for the first load; RSC is a component type whose code never ships.
State management choices
State
Reach for
Why
One component’s UI
useState
simplest; keep it close
Many actions, complex transitions
useReducer
pure, testable reducer
Shared by nearby components
lift state up
one source of truth
App-wide, rarely changing
Context
theme, locale, auth user
Large client state, many writers
Redux Toolkit
middleware, time-travel devtools
Global store, little boilerplate
Zustand (or Jotai)
selector subscriptions, no provider
Server data
TanStack Query, SWR, framework loaders
caching, dedupe, refetch, invalidation
Filters, tabs, pages
URL search params
shareable, survives reload
Patterns
Composition over inheritance: children and slot props; React never needs component inheritance.
Custom hooks extract stateful logic and replaced most HOCs and render props.
Compound components: <Tabs> owns the state and shares it via context with <Tabs.List>, <Tabs.Tab>, <Tabs.Panel>; the caller controls the markup.
HOC (withAuth(Page)) wraps to add props: prop collisions, wrapper hell. Render props (<Mouse render={(pos) => …} />) share state via a function prop: flexible but nests deeply.
function useDebouncedValue(value, delay = 300) { const [debounced, setDebounced] = useState(value); useEffect(() => { const id = setTimeout(() => setDebounced(value), delay); return () => clearTimeout(id); // each change cancels the pending timer }, [value, delay]); return debounced;}
JavaScript
Testing with Testing Library
Test behavior the user sees, not state or internals. Query priority: getByRole (with { name }) → getByLabelText → getByPlaceholderText → getByText → getByDisplayValue → getByAltText → getByTitle → getByTestId (last resort).
Variant
No match
One
Several
Async
getBy…
throws
returns
throws
no
queryBy…
null
returns
throws
no
findBy…
rejects
resolves
rejects
yes, retries (1 s default)
…AllBy…
throws / [] / rejects
array
array
as above
queryBy… asserts absence (.not.toBeInTheDocument()); findBy…/waitFor wait for things that appear later.
Prefer userEvent (real sequences: focus, keydown, input) over fireEvent; mock HTTP with MSW; test hooks with renderHook.
test('greets the user after submit', async () => { const user = userEvent.setup(); render(<Greeting />); await user.type(screen.getByLabelText(/name/i), 'Ada'); await user.click(screen.getByRole('button', { name: /submit/i })); expect(await screen.findByText('Hello, Ada')).toBeInTheDocument();});
JavaScript
Performance & security
Measure first: React DevTools Profiler, a production build, Chrome performance tracks (19.2+).
Keep state local; pass JSX as children; stable keys; split contexts and memoize provider values.
memo what the profiler flags (or adopt the Compiler); virtualize long lists; lazy routes.
Heavy updates go in useTransition/useDeferredValue; avoid parent → child fetch waterfalls.
XSS: JSX escapes text and attributes. dangerouslySetInnerHTML={{ __html: html }} bypasses that: pass only trusted or sanitized HTML (DOMPurify.sanitize(html)).
React 19 blocks javascript: URLs in href/src; still allow-list protocols for user-supplied links.
Server Functions are public HTTP endpoints: validate input, check authorization. Keep React patched (CVE-2025-55182, a critical Server Components RCE, was fixed in 19.0.1, 19.1.2 and 19.2.1).
Everything a 'use client' module imports ships to the browser: keep secrets server-side.
Quick answers
Virtual DOM? The element tree React keeps in memory; it diffs new against old and applies minimal DOM changes.
Props vs state? Props come from the parent, read-only; state is the component’s own memory.
Why isn’t state updated right after setX? The setter schedules a re-render; this render keeps its snapshot.
What triggers a re-render? Own state change, parent re-render, or a change in a context it reads.
Why keys? To match list items between renders so each keeps its state and DOM node.
Controlled vs uncontrolled? State holds the value (value + onChange) vs the DOM does (defaultValue).
useEffect vs useLayoutEffect? Layout effects run before paint and block it; useEffect generally after paint.
memo vs useMemo vs useCallback? Skip a re-render / cache a value / cache a function.
useRef vs useState? Both persist; a ref change doesn’t re-render.
Context vs Redux? Context passes a value down and re-renders all consumers; stores add selectors, middleware, devtools.
Function error boundary? Not possible: one class, or react-error-boundary.
Hydration?hydrateRoot attaches React to server HTML; the first client render must match it.
SSR vs Server Components? SSR renders HTML then hydrates; Server Components never send their code.
onClick={save()} calls save during render; pass save or () => save(id).
items.push(x); setItems(items) keeps the same reference: no re-render.
Stale closure: setInterval(() => setCount(count + 1), 1000) in a [] effect stops at 1; use setCount((c) => c + 1).
Object, array or function literals in deps re-run the effect every render.
A component declared inside another is a new type each render: it remounts and inputs lose focus.
A setter called unconditionally during render loops (“Too many re-renders”).
Date.now(), Math.random() or typeof window in render cause hydration mismatches.
key belongs on the outermost element returned from map.
Interview tip
“How would you speed this up?”: profile, move state down or lift content up, fix keys, split context, and only then add memo, useMemo and useCallback (or the React Compiler).