React 19.3 Beyond the Headlines: submitter, Fullscreen Props and Bundle Size
A practical tour of React 19.3's smaller changes: the submitter property, fullscreen event props, parallel Transitions, onReset, notable bug fixes and the real size increase.
This article is published in English.
Release notes tend to spotlight a handful of headline features, and React 19.3 is no exception. Below them sits a list of "other notable changes" plus a long page of bug fixes, most of them a single line that is easy to skip. Several of those lines, however, change how you write components today, explain bugs you may have already worked around, or affect what you ship to users. This guide goes through them roughly in order of how likely they are to matter to you, and finishes with a measured answer to the question everyone asks after an upgrade: how much bigger did the bundle get?
Knowing which button submitted a form
A form with two submit buttons, say Save and Save & close, needs to know which one the user pressed. Browsers have long answered that question: the native SubmitEvent exposes a submitter property pointing at the button that triggered the submission. React's synthetic event, though, did not pass it through. The usual workarounds were reading e.nativeEvent.submitter or, less gracefully, recording the last clicked button in state and hoping no keyboard submission slipped past. React 19.3 adds submitter to the synthetic submit event (#35590).
The easiest way to see the difference is to run one form against both versions. The demo starts with its imports; the namespace import is there only so the component can print React.version.
import { useState } from 'react';
import * as React from 'react';
The component prevents the default navigation, then writes the React version and the value of e.submitter into state. Each button carries its own value, which is exactly what you would branch on in real code.
export default function App() {
const [msg, setMsg] = useState('press either button');
return (
<form onSubmit={e => {
e.preventDefault();
setMsg(`${React.version}: e.submitter → ${e.submitter ? e.submitter.value : String(e.submitter)}`);
}}>
<button value="save">Save</button>{' '}
<button value="save-close">Save & close</button>
<p className="msg">{msg}</p>
</form>
);
}
A single rule styles the output line in a monospace font.
.msg { margin: 10px 0 0; font-family: ui-monospace, monospace; }
The same form on 19.3
The 19.3 version of the demo is identical, which is the point: only the React version changes. The imports are the same.
import { useState } from 'react';
import * as React from 'react';
So is the component.
export default function App() {
const [msg, setMsg] = useState('press either button');
return (
<form onSubmit={e => {
e.preventDefault();
setMsg(`${React.version}: e.submitter → ${e.submitter ? e.submitter.value : String(e.submitter)}`);
}}>
<button value="save">Save</button>{' '}
<button value="save-close">Save & close</button>
<p className="msg">{msg}</p>
</form>
);
}
And so is the stylesheet.
.msg { margin: 10px 0 0; font-family: ui-monospace, monospace; }
On 19.2 the message shows undefined, because the synthetic event has no such field. On 19.3 it shows save or save-close, depending on the button. Keep in mind which code path this affects. If you submit through form Actions instead of an onSubmit handler, React was already reading the submitter when it built the FormData (#29028), so the pressed button's name and value pair was already present in the action's data. The new property matters for event handlers.
A practical note: the submitter's value only helps if you actually set value (and usually name) on each button. A button without them produces an empty string, which is easy to mistake for a missing feature.
Fullscreen events as first-class props
The Fullscreen API dispatches fullscreenchange and fullscreenerror on the element involved. React did not recognize those event names, so components such as video players had to attach listeners manually with addEventListener inside an effect and remember to remove them on cleanup. React 19.3 adds onFullscreenChange and onFullscreenError as regular props (#34621).
The demo needs a ref for the element that goes fullscreen and a piece of state for the event log.
import { useRef, useState } from 'react';
Notice that both handlers sit directly on the div. The change handler checks document.fullscreenElement to decide whether the element just entered or left fullscreen, because the event itself fires in both directions. The Go fullscreen button also attaches a .catch to the promise returned by requestFullscreen(), since a refusal is reported both as a rejected promise and as an error event.
export default function App() {
const box = useRef(null);
const [log, setLog] = useState([]);
const add = line => setLog(l => [...l, line]); return (
<div
ref={box}
className="stage"
onFullscreenChange={() => add(document.fullscreenElement ? 'entered fullscreen' : 'left fullscreen')}
onFullscreenError={e => add('fullscreen error: ' + e.type)}
>
<button onClick={() => box.current.requestFullscreen().catch(err => add('rejected: ' + err.message))}>
Go fullscreen
</button>{' '}
<button onClick={() => document.fullscreenElement && document.exitFullscreen()}>Exit</button>
<pre className="log">{log.join('\n') || 'no events yet'}</pre>
</div>
);
}
The stylesheet uses the :fullscreen pseudo-class to center the content while the stage fills the screen, and light-dark() to pick a background that suits the color scheme.
.stage { padding: 12px; border-radius: 10px; background: light-dark(#f1f5f9, #262a33); }
.stage:fullscreen { display: grid; place-content: center; }
.log { margin: 10px 0 0; font-size: 14px; }
onFullscreenError fires when the browser declines the request. The two common causes are calling requestFullscreen() outside a user gesture, and running inside an <iframe> that lacks allow="fullscreen". If you embed a player in a third-party page and fullscreen silently does nothing, that missing iframe permission is the first thing to check, and the new prop gives you a clean place to log it.
Transitions no longer wait on each other
The changelog entry reads that each Transition is now rendered on its own rather than being folded together with others into one combined render, so that a slow Transition cannot block unrelated ones. The change (#37290) enables the enableParallelTransitions flag.
Before 19.3, if a second startTransition update arrived while React was still rendering the first, the two were merged. The in-progress render restarted with both updates applied, and neither could commit until that combined render completed. A quick, cheap Transition could therefore be held hostage by an expensive one it had nothing to do with. Now each Transition is its own unit of work. If you want to understand how React tracks those units internally, our article on how React lanes encode update priority covers the underlying model.
How you write Transitions does not change, and React's ordering guarantees still apply: a later update will never commit alongside older state. One assumption does become false, though. Updates issued through two different startTransition calls may now land in different frames. If several updates must appear together, put them in one startTransition call.
Do not expect to see this in a toy example. An attempt to reproduce it with a slow tree of 300 components and a trivial fast update scheduled 50 ms later showed the fast update committing after the slow one on both 19.2 and 19.3. The improvement lives at the scheduler level and shows up in large applications under real load, not in small demos.
Forms: onReset after an Action, and new error wording
When a form Action completes, React automatically resets the form. Until now it performed that reset with its event system switched off, which meant the reset event was swallowed. An onReset handler meant to clear controlled state never ran, and controlled inputs kept showing their previous values (#35176, which fixes issue #33630). In 19.3, onReset fires for the automatic reset as well. If you worked around the bug by listening for the native reset event yourself, you can return to the prop and delete the effect.
A related cosmetic change: useActionState error messages now refer to "action state" rather than "form state" (#35790). Behavior is unchanged, but log searches, alert rules or tests that match the old wording need updating.
Development-time changes: Strict Mode, use() and Fast Refresh
Three changes affect only development builds, yet each can surface something surprising after an upgrade.
- Strict Mode during hydration. Effects are now double-invoked while hydrating in Strict Mode (#35961), matching the behavior client-rendered roots have had since React 18. If a server-rendered app only looked Strict Mode safe because hydration skipped the second invocation, this will expose it. The fix is the usual one: make effects idempotent and give them proper cleanup.
- A warning for conditional
use(). Callinguseconditionally is allowed. The problem case is a component that suspended onuse(), then, on the retry, took a branch that never calleduse()at all. The Suspense boundary then resolves for the wrong reason. React now emits a development warning explaining that a library suspended throughuse()in an earlier render, skippeduse()once it completed, and that this is an incorrect use of the hook. The wording hints that the culprit is often a data library rather than your own component (#37104, #37203, #37491). - Fast Refresh fixes. Hot reloading now copes with three cases: a component loaded through
lazy(), a changedmemo()comparison function, and an edit that turns a component into a different kind of component (#36965, #36964, #36963, #36950). If those edits sometimes forced a full page reload, these were the causes.
Smaller DOM changes
resizeupdates are now batched until the next frame (#35117), the same way scroll events already were. State set in a resize listener no longer flushes early just because a click arrives in the middle of a frame.maskTypeis recognized as an SVG prop (#35921).- Module resources can now carry a priority hint: pass
fetchPrioritytoReactDOM.preloadModuleorReactDOM.preinitModuleand React renders it as afetchpriorityattribute (#36835). This lets an RSC bundler plugin mark client-component chunks as low priority. The non-modulepreloadandpreinitfunctions already supported it. credentiallessis treated as a boolean attribute on<iframe>(#36148), and React suggests the correct casing if you writecredentialLess(#36322).- React no longer reassigns
innerHTMLwhen it has not changed (#36949). Re-rendering with an identicaldangerouslySetInnerHTMLno longer makes the browser parse the markup again, which also preserves any DOM state inside it. defaultValueontype="number"inputs now behaves the same as on other input types (#36980).- The
toggleevent copies thesourceproperty for popovers (#37389).
Server rendering and Server Components
- Errors thrown on the server now reach the client with their
cause(#35810), and anAggregateErrorkeeps itserrorsarray (#36156). Error boundaries and logging code that walk the error chain see the same structure on both sides. - Server Components may now render
<Activity>, because Flight understands it (#34697). react-dom/serverno longer hangs under Deno (#35235), and the Bun entry point now includes every Node API (#34193).
Bug fixes whose symptoms you may have seen
The complete list is long. These are the entries most likely to match a bug you have already chased.
useDeferredValuecould get stuck on a stale value (#36134). A deferred value that occasionally never caught up was a React bug, not a mistake in your code.- Context changes did not always reach Suspense fallbacks (#36160) or a subtree behind a boundary that was still suspended (#35839).
useEffectEventinsideforwardRefandmemocomponents read stale values (#34831).- While an
<Activity>tree was hidden,useSyncExternalStoremissed store mutations (#36947). In addition, a hidden tree stops hoisting its<title>(#34983), keeps its thrown errors out of the visible UI (#35074) and no longer shows what it renders into portals (#35091). - Form status was reset when unrelated state changed (#34075).
- Apps using CSP nonces saw a false hydration mismatch warning on
nonceattributes (#37030). - Focus handling was fixed for delegated elements and elements that were already focused (#36010).
- Two
<ViewTransition>crashes were fixed, one in Mobile Safari (#35337) and one when combined withSuspenseList(#35520). These matter if you tried the experimental builds. - On the Server Components side,
decodeReplyFromBusboydroppedFormDataentries (#36468), deep async chains could overflow the stack (#35612), and debug information could grow exponentially until it threw aRangeError(#37481).
The full set of pull requests is linked from the React CHANGELOG.
What the upgrade costs in bytes
Shortly after the release, a comment in the r/reactjs discussion noted a jump in react-dom-client.production.js from 523.5 kB to 610.5 kB and asked for the reason. Comparing the 19.2.0 and 19.3.0 packages as published on unpkg gives these figures:
cjs/react-dom-client.production.js, raw: 536,016 B in 19.2.0 versus 625,168 B in 19.3.0, an increase of 89 kB (16.6 %).- The same file with gzip at level 9: 94.7 kB versus 110.5 kB, an increase of 15.7 kB.
cjs/react-dom-server.node.production.js, raw: 277,046 B versus 300,544 B, an increase of 23.5 kB.cjs/react.production.js, raw: 17,217 B versus 18,040 B, an increase of 0.8 kB.
The numbers in the Reddit comment are the same sizes expressed in KiB. The cause is straightforward. View Transitions, Fragment Refs, browser() and the Trusted Types code path used to sit behind feature flags that removed them from the stable build. In 19.3 they ship in it. Over the network the difference is roughly 16 kB gzipped, comparable to a medium-sized icon set.
That is a genuine cost worth tracking in your performance budget, but it is not a regression in how React behaves. If the extra bytes matter for your app, the more productive levers are usually code splitting, trimming your own dependencies and deferring non-critical client components rather than holding back the upgrade.
Key takeaways
- Read
e.submitterdirectly inonSubmithandlers; form Actions already received the button'snameandvalue. - Replace manual
fullscreenchangelisteners withonFullscreenChangeandonFullscreenError, and handle the rejected promise fromrequestFullscreen()too. - Separate
startTransitioncalls may now commit in different frames, so group updates that must appear together. onResetfinally runs after an Action's automatic reset, and Strict Mode now double-invokes effects during hydration, so re-check effect cleanup.- Several long-standing bugs, including a stuck
useDeferredValueand staleuseEffectEventvalues, are fixed; check whether any workarounds in your code can go. - Budget about 16 kB gzipped extra for
react-domon the client, the price of formerly flagged features now included in stable builds.