back to blog

React 19.3: the features you'll actually use

View Transitions and Fragment Refs leave experimental, plus use(browser()), Trusted Types, and Context rendered directly in Server Components. A code example for each

#react#react-19

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, and blur move focus through the children, depth-first
  • addEventListener, removeEventListener, and dispatchEvent on first-level children
  • observeUsing and unobserveUsing hook up an IntersectionObserver or ResizeObserver
  • getClientRects, scrollIntoView, and compareDocumentPosition for 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 use call, it can go inside an if. 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.