React 19.3 shipped on September 9, 2026. View Transitions and Fragment Refs, experimental since 2025, are now stable. react-dom also gained use(browser()) and Trusted Types support, and Server Components can now render <Context> directly.
Below is a code example for each one.
1. View Transitions
import { ViewTransition, startTransition } from "react";
function Gallery({ selected, onSelect }) {
return (
<ViewTransition>
{selected ? (
<FullscreenPhoto photo={selected} />
) : (
<Grid onSelect={(id) => startTransition(() => onSelect(id))} />
)}
</ViewTransition>
);
}
<ViewTransition> animates its children with the browser's native View Transition API. React picks the animation based on how the tree changed:
- enter: the
<ViewTransition>was mounted - exit: the
<ViewTransition>was unmounted - update: its children changed content or style (the example above)
- share: a named
<ViewTransition>left one place and showed up in another
The detail that trips people up: only updates marked as Transitions animate. That means startTransition, a <Suspense> reveal, or useDeferredValue. A plain setState counts as urgent and shows up instantly, with no animation.
The default is a cross-fade. To customize it, pass a CSS class (enter="slide-in") or use the onEnter, onExit, onUpdate, and onShare events with the Web Animations API.
A different animation per cause: addTransitionType
In a carousel, going forward and going back change the same state (currentSlide), but the animation has to go in opposite directions. addTransitionType tags the reason for the update inside startTransition:
import { ViewTransition, addTransitionType, startTransition } from "react";
function Carousel({ slides, current, setCurrent }) {
function go(direction, nextIndex) {
startTransition(() => {
addTransitionType(direction);
setCurrent(nextIndex);
});
}
return (
<>
<button onClick={() => go("previous", current - 1)}>Previous</button>
<button onClick={() => go("next", current + 1)}>Next</button>
<ViewTransition
key={slides[current].id}
enter={{ next: "from-right", previous: "from-left" }}
exit={{ next: "to-left", previous: "to-right" }}
>
<Slide slide={slides[current]} />
</ViewTransition>
</>
);
}
The key makes each slide a new <ViewTransition>, so the outgoing one runs exit and the incoming one runs enter. The objects map each type to a CSS class, where you define the animation.
React also passes the type on to the browser, so you can do it all in CSS:
:active-view-transition-type(next) {
/* "next" animations */
}
2. Fragment Refs
import { Fragment, useEffect, useRef } from "react";
function WizardStep({ step, children }) {
const stepRef = useRef(null);
useEffect(() => {
stepRef.current.focus();
}, [step]);
return <Fragment ref={stepRef}>{children}</Fragment>;
}
You can now pass ref straight to a <Fragment>. Instead of a DOM node you get a FragmentInstance, which works on the children as a group:
focus,focusLast, andblurmove focus through the children, depth-firstaddEventListener,removeEventListener, anddispatchEventon first-level childrenobserveUsingandunobserveUsinghook up anIntersectionObserverorResizeObservergetClientRects,scrollIntoView, andcompareDocumentPositionfor measuring and scrolling
No more wrapper <div> just to hang a ref on, which sometimes broke layouts (flex, grid, :first-child). It also works when the child is a library component that doesn't forward ref.
3. use(browser())
"use client";
import { Suspense, use } from "react";
import { browser } from "react-dom";
function ThemeFromStorage() {
use(browser("theme is stored in localStorage"));
const theme = localStorage.getItem("theme") ?? "light";
return <p>Current theme: {theme}</p>;
}
export function ThemeLabel() {
return (
<Suspense fallback="Loading theme...">
<ThemeFromStorage />
</Suspense>
);
}
This marks a component as browser-only. During SSR, React skips it and puts the nearest <Suspense> fallback in the HTML. On the client, use(browser()) returns undefined and the component renders normally.
It replaces the two usual tricks:
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
const isBrowser = typeof window !== "undefined";
The difference is that the component now takes part in Suspense like any other, so its loading state coordinates with the rest of the page.
Three rules:
- It needs a
<Suspense>above it, or the server render fails - With Server Components (Next.js App Router), it only works in a Client Component
- Like any
usecall, it can go inside anif. For example, you can skip SSR only when there's no initial data
4. Trusted Types
import DOMPurify from "dompurify";
function Comment({ userContent }) {
const html = DOMPurify.sanitize(userContent, { RETURN_TRUSTED_TYPE: true });
return <div dangerouslySetInnerHTML={{ __html: html }} />;
}
Trusted Types is a browser defense against DOM-based XSS. With the Content-Security-Policy: require-trusted-types-for 'script' header, the browser refuses raw strings in sinks like innerHTML and only accepts TrustedHTML objects created by one of your policies.
React used to coerce everything to a string ('' + value) before handing it to the DOM, which turned TrustedHTML back into a string that the browser then blocked. Now the value passes through untouched, so your policy actually works.
5. <Context> directly in Server Components
A Server Component can't create a Context, but it can render one imported from a "use client" module. Up to 19.2, that took a Provider component whose only job was passing a prop along:
// user-context.jsx
"use client";
import { createContext } from "react";
export const UserContext = createContext(null);
export function UserProvider({ currentUser, children }) {
return <UserContext value={currentUser}>{children}</UserContext>;
}
In 19.3 UserProvider goes away. The Server Component imports the Context and renders it directly:
// user-context.jsx
"use client";
import { createContext } from "react";
export const UserContext = createContext(null);
// app/layout.jsx (Server Component)
import { UserContext } from "./user-context";
export default async function Layout({ children }) {
const currentUser = await getCurrentUser();
return <UserContext value={currentUser}>{children}</UserContext>;
}
This is a common pattern in the Next.js App Router: fetch the data in a layout and share it with the Client Components below. One less component to write.
Upgrading
With whichever package manager you use:
npm install react@19.3 react-dom@19.3
pnpm add react@19.3 react-dom@19.3
yarn add react@19.3 react-dom@19.3
bun add react@19.3 react-dom@19.3
The release lists no breaking changes or deprecations. There's a long list of fixes worth skimming, including a <ViewTransition> crash on mobile Safari.