skip to content

Server Components and Suspense

React's rendering model now spans two runtimes: components that run only on the server and components that ship to the browser, stitched together by Suspense boundaries and streamed HTML. Interviewers press on this because it changes where data is fetched, what lands in the bundle, and why hydration errors appear at all.

part ofReactoverview, primer and where to startread it →
on this pageshow

explore

questions

page 1 of 2

In React 19 you can pass a function to a <form> element's action prop instead of writing an onSubmit handler. What does React take over on your behalf when you do that?

level: juniorimportance: must knowfreq 62%

answer

  1. React, not you, calls preventDefault
  2. the argument is not the event
  3. it runs inside a transition
  4. pending becomes readable by descendants
  5. uncontrolled fields clear on success

basics

~20 s

React calls the function with the form's FormData, prevents the default navigation itself, and runs the call as an Action inside a transition — so React tracks pending state, surfaces errors, and resets an uncontrolled form after it succeeds.

solid answer

~50 s

The `action` prop has always accepted a URL string; React 19 also accepts a function. When it is a function, React intercepts the submit for you: it prevents the browser's default navigation, builds a `FormData` object from the form's named fields, and calls your function with it. Because React runs that call inside a transition, the function may be `async` and React keeps the submission marked pending for its whole duration — which is what makes descendant pending UI (via `useFormStatus` from `react-dom`) and optimistic updates possible without threading props. If the function rejects and you do not handle it, React surfaces the error to the nearest error boundary rather than silently dropping it. After a successful function action React resets an uncontrolled form's fields. The contrast with `onSubmit` is ownership: with `onSubmit` you call `preventDefault()`, read the fields, and hand-roll a loading flag; with `action` React owns all four.

code

jsx · 17 lines
jsx
function Subscribe() {
  async function subscribe(formData) {
    const email = formData.get('email');
    await fetch('/api/subscribe', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email }),
    });
  }

  return (
    <form action={subscribe}>
      <input type="email" name="email" required />
      <button type="submit">Subscribe</button>
    </form>
  );
}

go deeper

for a junior

Know that a function passed to a form's action prop receives a FormData object, that React blocks the browser's default navigation for you, and that the function may be async.

for a middle

Explain that React runs the action inside a transition, and that this is what makes pending state readable by descendants and optimistic updates possible — not merely a nicer way to read field values.

for a senior

Be ready to say what React does not do: no double-submit protection, no validation, no retry, and no use of the return value unless the action is routed through useActionState. Name the uncontrolled-form reset and when it hurts.

for a principal

Own the convention: whether forms in your codebase go through Actions at all, what shape an action's result takes so error rendering is uniform, and how a shared submit component reads pending state without every form reinventing it.

## Two shapes of the same prop On a DOM `<form>`, `action` is an HTML attribute: a URL the browser posts to, causing a full-page navigation. React 19 keeps that meaning when you pass a string, and adds a second meaning when you pass a function. The function form is what React calls an **Action**. When React sees a function, it attaches its own submit handling to the form. On submit it calls `preventDefault()` for you — so the page does not navigate — collects the form's fields into a `FormData` object, and invokes your function with that object as its only argument. Fields are collected by their `name` attribute, exactly as the browser would for a native submission, which means an input without a `name` contributes nothing. ```jsx function Subscribe() { async function subscribe(formData) { const email = formData.get('email'); await fetch('/api/subscribe', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email }), }); } return ( <form action={subscribe}> <input type="email" name="email" /> <button type="submit">Subscribe</button> </form> ); } ``` ## Why it is more than sugar for onSubmit The part that matters is not the `FormData` convenience — it is that React runs the call inside a **transition**. A transition is React's marker for "an update whose result the user is waiting on, but which must not block the interface". Because the submission is a transition, React knows the moment it starts and the moment it ends, including across `await` points inside an `async` action. That single fact is what the rest of the Actions model is built on: - **Pending state becomes ambient.** Any descendant of the form can ask React whether its parent form is currently submitting, using `useFormStatus` (imported from `react-dom`). A shared `SubmitButton` in a design system can render its own spinner without the form threading an `isSubmitting` prop down to it. - **Optimistic updates have a lifetime.** An optimistic value shown during the Action is automatically discarded when the transition finishes, because React knows when that is. - **Errors have a destination.** If the function rejects and nothing catches it, React surfaces the error to the nearest error boundary instead of leaving an unhandled rejection in the console. With a hand-written `onSubmit` you can reproduce every one of these, but each one is code you own: a `try/finally` so the loading flag is not left stuck `true`, a piece of state and a prop chain for the spinner, and your own decision about what an unhandled rejection means. ## What happens after the action resolves For a function action, React resets an **uncontrolled** form after the action completes successfully — the typed values disappear, which is usually what you want for a "post a comment" box and surprising for a long edit form. Controlled inputs are unaffected, because their values live in your state, not in the DOM node. React 19 exposes `requestFormReset` from `react-dom` when you need to control the reset yourself. Nothing else is reset or retried. React does not disable the submit button, does not deduplicate rapid submissions, and does not decide what the result means. Those remain design decisions in your component. ## The pieces you still own An Action is a plumbing primitive, not a form library. You still decide: - **What the action returns.** A returned value only becomes visible state if you route the action through `useActionState`; a bare `<form action={fn}>` discards the return value. - **How failures are shown.** Rejecting is a blunt instrument (it reaches an error boundary); returning an error result is how inline field messages are normally produced. - **Validation.** React does not validate; native constraint attributes such as `required` and `type="email"` still work and still block submission before your function runs. ## Where candidates trip The two most common mistakes are expecting the submit **event** as the argument (it is `FormData`; there is no `event.preventDefault()` to call, and nothing to read `event.target` from) and assuming the function form only works inside a server framework. It does not: a plain client-side function is a perfectly valid action in any React 19 app. What a server framework adds is the ability for the same form to work before hydration, which is a separate property. Finally, a submit `<button>` may carry its own `formAction` function to override the form's, which is the React 19 way to give one form two verbs — "save draft" and "publish" — without inspecting which button was clicked.

  • If the action rejects, does React still reset the form?
    No. The automatic reset applies after a function action completes successfully, so a rejected submission leaves the typed values in place — which is the behaviour you want, since the user usually has to correct something and resubmit. If you need to control resetting yourself, React 19 exposes `requestFormReset` from `react-dom`.
  • What changes if you pass a plain URL string to action instead of a function?
    You get the ordinary HTML behaviour: React adds no handling at all, the browser performs a real navigation and posts the fields to that URL, and the React app on the page is torn down and reloaded. There is no pending state, no optimistic UI, and no return value — the server response is a new document.
  • How would you let one form have two different submit actions?
    Give each submit button its own `formAction` prop. In React 19 `formAction` on a `<button>` or `<input type="submit">` accepts a function and overrides the form's `action` for that button, so a "Save draft" and a "Publish" button can run different functions over the same fields without inspecting which one was pressed.

saying these in an interview costs you the question

  • Says you must call event.preventDefault() inside the action
  • Expects the submit event as the action's argument
  • Thinks the action must be synchronous
  • Claims form actions only work in a server framework
  • Believes React resets controlled inputs after submission

context

open as a page

In React, what does the fallback prop of <Suspense> render, and what decides when the user sees the fallback instead of the boundary's children?

level: juniorimportance: must knowfreq 70%

basics

~20 s

The fallback prop holds the UI React shows in place of a <Suspense> boundary's children while something inside those children is not ready yet. React swaps the real children back in as soon as nothing inside that boundary is suspended.

open as a page

In React 19, props passed from a Server Component to a Client Component are serialized into the RSC payload before they reach the browser. Which kinds of values survive that trip, and which ones make React throw?

level: juniorimportance: must knowfreq 68%

basics

~20 s

Primitives, plain objects, arrays, Date, Map, Set, typed arrays, JSX elements, promises and Server Functions can cross. Ordinary functions, class instances and locally created symbols throw, because React must write every prop into a wire payload the browser reads back.

open as a page

In a React 19 app that uses React Server Components, what is the fundamental difference between a Server Component and a Client Component?

level: juniorimportance: must knowfreq 78%

basics

~20 s

Server Components execute only on the server, once per request, and their code never ships to the browser. Client Components are sent to the browser as JavaScript, where they mount, hold state, and respond to user input.

open as a page

In a React Server Components app, a file that starts with 'use client' imports a Button from another file that has no directive. Is that Button a client component, does its own file need a 'use client' line, and where in a file must the directive be written?

level: juniorimportance: must knowfreq 62%

basics

~20 s

Yes, the Button is a client component. Files below a client boundary inherit it through imports and need no directive of their own. The directive must be the first statement of a file, above every import.

open as a page

In a server-rendered React app, what is a hydration mismatch, and what does React 19 do when it detects one?

level: juniorimportance: must knowfreq 66%

basics

~20 s

A hydration mismatch means the markup React produced on the server differs from what the same components produce on the client's first render. React 19 logs a hydration error and re-renders the affected subtree on the client, throwing away that server HTML.

open as a page

A React app serves server-rendered HTML, then the JavaScript bundle loads and calls hydrateRoot. What does React actually do to that existing HTML during hydration, and what does it deliberately not do?

level: juniorimportance: must knowfreq 70%

basics

~20 s

Hydration reuses the server's DOM instead of rebuilding it: React renders the component tree on the client, matches it against the existing nodes, attaches event listeners at the root container, fills refs, then commits and runs effects.

open as a page

A React 19 Client Component contains `const user = use(fetch('/api/user').then(r => r.json()))` directly in its body. The UI never leaves the loading fallback and the network panel fills with repeated requests to /api/user. What is happening, and what fixes it?

level: middleimportance: must knowfreq 52%

basics

~20 s

Each render calls fetch again, so use() receives a brand-new pending promise every time. React retries the render when a promise settles, the retry starts another request, and it suspends again — an endless fetch-suspend-retry loop.

open as a page

When a React component suspends, which <Suspense> boundary shows its fallback, and how does nesting boundaries change how much of the screen is replaced?

level: middleimportance: must knowfreq 62%

basics

~20 s

The nearest <Suspense> boundary above the suspended component shows its fallback; boundaries further up are unaffected. Nesting boundaries therefore shrinks the blast radius, letting an inner section swap to a small placeholder while everything outside it stays on screen.

open as a page

During React 19 streaming server rendering, what is the "shell" of the response, and by what mechanism does the HTML for a slow subtree inside a <Suspense> boundary reach the browser after the shell has already been sent?

level: middleimportance: must knowfreq 58%

basics

~20 s

The shell is everything outside any still-pending Suspense boundary; React flushes it first, with each pending boundary represented by its fallback markup. When a slow subtree resolves, React streams its real HTML inside a hidden container plus a tiny inline script that moves those nodes into the boundary's position and removes the fallback.

open as a page

A React Server Component renders `<SaveButton onSave={() => saveRow(id)} />`, where SaveButton is a Client Component, and the render fails with an error about functions. Why does React reject that prop, and what are the legitimate ways to give that button behaviour?

level: middleimportance: must knowfreq 72%

basics

~20 s

A prop crossing into a Client Component is serialized and sent to the browser, and a function is code plus closure over server scope, so it cannot travel. Define the handler in the client module, or pass a Server Function marked 'use server'.

open as a page

Why can a React Server Component not call useState or useEffect, or attach an onClick handler to the DOM elements it renders?

level: middleimportance: must knowfreq 68%

basics

~20 s

A Server Component runs once on the server and then no longer exists, so nothing in the browser can re-render it, commit an effect, or run a handler for it. React rejects those hooks and tells you to add 'use client'.

open as a page

In a React Server Components app, an interactive collapsible panel is a client component and needs to display a server component that queries the database. Why can't the panel import that server component, and what composition pattern lets the server-rendered content appear inside it?

level: middleimportance: must knowfreq 66%

basics

~20 s

Importing it would pull the server component into the client bundle, so it could no longer run on the server. Instead a server parent renders it and passes it into the panel as children or a slot prop — already rendered, so it crosses as output, not as code.

open as a page

In a React app using Server Components, what does the 'use client' directive at the top of a module actually do to the module graph, and why is it described as a boundary rather than a per-component switch?

level: middleimportance: must knowfreq 75%

basics

~20 s

'use client' marks a module as an entry point into the client bundle: that file and everything it imports become client code. It opens a boundary in the module graph rather than switching one component into client mode.

open as a page

In React 19, what is the difference between calling hydrateRoot(container, <App />) and createRoot(container).render(<App />), and what goes wrong if you use createRoot on a container that already holds server-rendered HTML?

level: middleimportance: must knowfreq 58%

basics

~10 s

createRoot renders a fresh tree into a container; hydrateRoot adopts server-rendered HTML already inside it, reusing those nodes. Calling createRoot on server HTML discards that markup and re-renders from scratch, wasting the server work.

open as a page

A server-rendered React component renders <span>{new Date().toLocaleTimeString()}</span>, and the browser console reports a hydration mismatch on every page load. Explain the cause and how you would show the local time without the mismatch.

level: middleimportance: must knowfreq 72%

basics

~20 s

The server and browser call new Date() at different moments, in different timezones and locales, so the two renders produce different text. Render a stable server-provided value first and switch to the localized time in an effect after mount.

open as a page

A React page rendered on the server with react-dom/server's renderToString contains a Suspense boundary whose child awaits a slow database query. What HTML does the browser receive, and what changes if you switch to renderToPipeableStream?

level: middleimportance: must knowfreq 62%

basics

~20 s

renderToString buffers the whole page and never waits for suspended data, so the boundary ships as its fallback and the response leaves only after the full render. renderToPipeableStream flushes that same shell immediately, then streams the real content on the same response when the query resolves.

open as a page

A React list that is already on screen flips back to its <Suspense> fallback every time the user changes a filter that triggers a new data read. Why does the revealed content disappear, and how do you keep it visible while the new data loads?

level: seniorimportance: must knowfreq 58%

basics

~20 s

The filter change is an urgent update, so re-suspending the boundary immediately swaps the revealed list for its fallback. Marking that update as a transition with startTransition or useTransition tells React to keep the old content on screen until the new data is ready.

open as a page

In React server rendering, what does it mean to "stream" the HTML rather than render the whole page before sending anything, and what does streaming actually improve for the user?

level: juniorimportance: should knowfreq 50%

basics

~20 s

Streaming server rendering sends HTML in pieces as it becomes ready instead of buffering the whole page. React flushes the parts that are done immediately and delivers slow, Suspense-wrapped sections later, so the first bytes and first paint arrive much sooner.

open as a page

In react-dom/server, what is the difference between renderToString and renderToStaticMarkup, and when would you reach for the static one?

level: juniorimportance: should knowfreq 40%

basics

~20 s

renderToString emits React's hydration bookkeeping — comment markers around Suspense boundaries and separators between adjacent text nodes — so the client can attach to the markup. renderToStaticMarkup omits all of it, producing plain HTML for output React will never hydrate.

open as a page

React 19 calls any async function run inside startTransition an Action. For a mutation with no form involved — a Delete button that calls an API and then updates state — what does running it as an Action buy you over an async onClick handler that manages its own isLoading state?

level: middleimportance: should knowfreq 45%

basics

~20 s

React owns the pending state: the transition stays pending across every await until the async function settles, errors reach the nearest error boundary, optimistic values revert on their own, and the resulting updates are non-blocking rather than urgent.

open as a page

A React 19 chat UI shows a message optimistically while the send Action is in flight. If that Action rejects, what does the user actually see, and what does the 'rollback' consist of in code?

level: middleimportance: should knowfreq 40%

basics

~20 s

Rollback is not an undo step. The optimistic value exists only while the Action is pending; when it rejects React re-renders from the real state, which never changed, so the temporary message simply disappears. Showing the error is your job.

open as a page

In React 19, what actually happens inside React when a component suspends while rendering, and what does that imply about the work the component had already done in that render?

level: middleimportance: should knowfreq 44%

basics

~20 s

Suspending unwinds that component's render: React abandons the in-progress work, shows the nearest Suspense fallback, subscribes to the pending promise, and runs the component again from the top when it settles. Nothing it rendered is committed.

open as a page

In a React Server Component you fetch a record with an ORM and pass the returned model instance straight into a Client Component as a prop, and React throws that only plain objects can be passed. Why does a Date prop work but the model instance not, and how do you fix it?

level: middleimportance: should knowfreq 48%

basics

~20 s

The RSC payload has built-in encodings for a fixed set of types including Date, but any other object must be a plain object built from an initializer. A class instance is defined by its prototype and methods, which cannot travel, so map it to a plain shape first.

open as a page

A React Server Component can be declared as an `async function` and `await` data directly in its body. What does React do with that, and why can a Client Component not be written the same way?

level: middleimportance: should knowfreq 55%

basics

~20 s

React awaits the promise as part of rendering that subtree and renders the component once the data arrives, so no loading flag or effect is needed. On the client, React 19 does not support async function components.

open as a page

In a React app that uses Server Components together with server-side rendering, where does a Client Component actually execute?

level: middleimportance: should knowfreq 58%

basics

~20 s

Client Components usually run twice: once on the server during server rendering to produce the initial HTML, then in the browser, where they hydrate and handle every later update. 'Client' means shipped and interactive, not skipped on the server.

open as a page

A server-rendered React component renders a <div> inside a <p>. The markup uses no dates, randomness or browser APIs, yet hydration still reports a mismatch. What causes it?

level: middleimportance: should knowfreq 44%

basics

~20 s

The browser's HTML parser cannot nest a <div> inside a <p>, so it closes the paragraph early and makes the div a sibling. React then hydrates against a DOM whose shape differs from the tree it is rendering, and reports a mismatch.

open as a page

What does React's suppressHydrationWarning attribute do to a server-rendered element, how far does its effect reach, and when is reaching for it the right call?

level: middleimportance: should knowfreq 38%

basics

~20 s

suppressHydrationWarning tells React not to report differences in that one element's text and attributes between the server HTML and the first client render. It reaches only one level deep, changes nothing about the rendering itself, and is an escape hatch for differences you intend.

open as a page

react-dom/server exposes both renderToPipeableStream and renderToReadableStream. How do the two differ, and how do you choose between them?

level: middleimportance: should knowfreq 45%

basics

~20 s

They produce the same streamed HTML for different runtimes. renderToPipeableStream targets Node.js streams: it returns { pipe, abort } synchronously and you pipe into the server response. renderToReadableStream targets Web Streams: it returns a promise for a ReadableStream you pass to a Response.

open as a page

An async function passed to a React 19 <form> element's action prop rejects because the API returned a validation error. What does React do with that rejection by default, and how should the Action be designed so the user sees an inline field message instead?

level: seniorimportance: should knowfreq 42%

basics

~20 s

React does not swallow it: an uncaught rejection inside an Action is surfaced to the nearest error boundary, which replaces the subtree. Treat validation failures as data — catch inside the Action and return an error result the form renders inline.

open as a page

showing 1–30 of 47