Add ReactDOM browser() API - #37143
Merged
Merged
Conversation
gnoff
force-pushed
the
jstory/react-dom-cache
branch
11 times, most recently
from
July 30, 2026 13:50
b818d42 to
dba2ef2
Compare
acdlite
approved these changes
Jul 30, 2026
gnoff
force-pushed
the
jstory/react-dom-cache
branch
11 times, most recently
from
July 30, 2026 18:16
54c34cb to
6a1614c
Compare
Export browser() from the client-capable react-dom entry points while omitting it from the react-server condition, so importing it into a Server Component remains a compile-time error.
Model the opaque result as a React recoverable Error branded with Symbol.for("react.recoverable"). Fiber is the final renderer and continues through recoverables without producing a value, while Fizz defers them to a downstream renderer.
When Fizz encounters use(browser()), eagerly capture an error at the use() site whose cause is the Error created by browser(), stash it, and throw a private sentinel to unwind the render. A Suspense boundary consumes that sentinel silently; if it reaches the root, substitute the stashed use-site error so the primary stack points to use() and the cause points to browser().
Encode the silent Fizz recovery with the internal empty digest and suppress the expected hydration report without reserving an ordinary userspace digest. In development, serialize a renderer-neutral deferral breadcrumb and component stack without exposing the sentinel stack. Directly throwing the browser() value remains a normal reported error because the recovery behavior is defined by use().
Cover the public browser API in Fiber, Fizz, hydration, root-fatal, stack and cause, direct-throw, development breadcrumb, production, WWW, digest, and module-scope scenarios.
gnoff
force-pushed
the
jstory/react-dom-cache
branch
from
July 30, 2026 18:20
6a1614c to
b0f262c
Compare
Treat a recoverable passed to abort() as a request to client render every unfinished Suspense boundary. These boundaries use the same silent wire signal as use(browser()), so neither the server onError callback nor the client's onRecoverableError callback is invoked. If the abort happens before the shell completes, report the same fatal root error as use(browser()) outside Suspense, with the browser() value as its cause. Add public streaming and hydration coverage for multiple pending boundaries and for the pre-shell fatal case. Also gate DEV-only wire diagnostics correctly and fix Flow refinements exposed by adding recoverables to Usable.
Add an enableBrowserAPI kill switch around the browser export while leaving it enabled in every feature flag fork. This provides a stable-release escape hatch without changing the recoverable rendering behavior when the API is enabled. Mark the browser-specific public API tests with the same gate so disabling the export remains a supported test configuration.
gnoff
force-pushed
the
jstory/react-dom-cache
branch
from
July 30, 2026 19:02
002b62d to
3d891e4
Compare
acdlite
approved these changes
Jul 30, 2026
acdlite
approved these changes
Jul 30, 2026
Describe the recoverable transition as an explicit request so the development breadcrumb applies to both use(browser()) and abort(browser()). Remove the test's direct inspection of hidden Suspense template metadata; the public behavior is already covered by the rendering and recovery assertions.
Keep the component-specific message for recoverables raised by use(browser()), and encode a separate message when abort(browser()) moves pending boundaries to client rendering. Mark recoverable aborts as aborted in both normal and replay paths so boundary encoding can reliably select the abort-specific diagnostic.
eps1lon
approved these changes
Jul 30, 2026
Emasoft
added a commit
to Emasoft/ai-maestro-integrator-agent
that referenced
this pull request
Aug 18, 2026
…T gate (TRDD-ONCGHA1Q) The AI review refuted the premise of both the original card and b201375: `gh pr view --json commits` returns commits CHRONOLOGICAL (oldest-first), verified live against react/react#37143 and microsoft/vscode#200000 (committedDate ascends). The original code's COMMENT was wrong but its COMPARISON was right; b201375 "normalized" on the comment's false premise and inverted a behaviorally-correct gate for one commit. Behavior now proven IDENTICAL to the original pre-fix code on all 6 table cases (executed both, extracted verbatim from git show 50b044f). Net change vs original: truthful comments pinning the VERIFIED ordering, and a test suite whose inputs encode gh's real shape — not the comment's. Why the audit missed it: the code comment, the audit worker, the card, and the first test all shared ONE unverified premise; executing the function on synthetic inputs derived from that premise "proved" the finding. A test whose inputs inherit an unverified premise validates the premise, not the code.
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a new API to
react-domcalledbrowser().browser()returns a "usable" that will error during SSR and resolve during rendering in the browser. The purpose is to allow you to express the idea that a component should suspend on the server but not in the browser. The method is not available inside areact-serverenvironment. This is a client only feature.This is a
react-domAPI because the concept of browser doesn't apply generally to React itself.This codifies a pattern that is common in some apps where you error during SSR to prevent rendering some component on the server and you end up suppressing the error that is reported in the client to avoid this appearing like a problem rather than intended behavior. Unfortunately this is not an option for many because hacking around to prevent errors from being logged is not practical for many
By making this a React API we enabled this common pattern in any React using library or application
It is an error to
use(browser())outside of a Suspense boundary because you cannot recover from the root. this restriction may be lifted in the future but is part of the current limitations of the APIImplementation
Deferring rendering to a downstream system is modeled in React already as recoverable errors. The idea is that in some environments you might not want to report something directly as an error because a later environment has an opportunity to recover from it without alerting the user to the mishap. This concept also shows up in RSC with halted references. They can "recover" in a later render by eventually resolving to some value.
To model the idea of "render in the browser" we are really just modeling an intentional recoverable error. However since you don't want to treat this kind of error as exceptional we intentionally suppress logging. Additionally since aborting a server render is semantically equivalent to "erroring" in every unfinished task we also support aborting with a
browser()so you can describe ending a stream with intentional holes that won't be logged as errors in the browser when hydrating.One interesting thing we do with this particular API is it returns an object that is isomorphic and it's the
useorabortfunction that handles differing behaviors. This means you can create these objects in module scope and use them even in complex scenarios like server rendering inside the browser while React is rendering.This implementation is flagged so we can disable the feature quickly if we decide to not ship this in a stable. It is going into React unprefixed for now because the semantics are clear and the utility is widely known.
Alternatives
We considered
useBrowser()or a similar hook however this means you must call it unconditionally. There are use cases where props might influence whether you want to allow something to render during SSR or not. for instance you might have a data fetching library that accepts initial data on the server but if it doesn't receive initial data it falls back to browser only rendering.Another consideration is a throwing function like just calling
browser()would throw if called during an SSR render. The main reason we do not think this is a good idea is because you can then call this arbitrarily deep and the throw can be caught and might be suppressed accidentally. By making it a usable it can only be done in hooks or hook-like contexts.