Search
SW News

React 19.3 Tutorial: 11 Steps to Ship in 60 Min [2026]

Yusuf Demir
Yusuf DemirCloud & Software Reporter
21 min read
React 19.3 Tutorial: 11 Steps to Ship in 60 Min [2026]

React 19.3 shipped on npm on September 9, 2026, and it changes how you’ll write animations, refs, and server-aware components in everyday React code. If you’ve been putting off the upgrade, or you just want a working project that actually uses the new APIs instead of a changelog summary, this tutorial walks through all of it: install, migrate, build, test, and ship. By the end you’ll have a small but complete React 19.3 app wired up with <ViewTransition>, Fragment Refs, browser(), and Trusted Types, plus the troubleshooting notes you’ll need when something doesn’t behave the way the docs imply.

This guide targets developers who already know basic React (components, hooks, JSX) but haven’t touched the 19.3 release yet. Total build time: roughly 45-60 minutes if you follow along step by step, less if you skim to the sections you need.

Google · Preferred Sources

Don't miss new tech stories on Google

Add TrendinTech once in the Google app and our stories appear in your news suggestions.

Add Now

What’s actually new in React 19.3

The official React 19.3 release post lists four headline additions: View Transitions, Fragment Refs, the browser() API, and Trusted Types support, alongside Context support inside Server Components. None of these are breaking changes — React 19.3 is a minor version bump from 19.2, and existing 19.x apps keep working without modification. That matters for planning your upgrade: you don’t need a big-bang rewrite, you can adopt each feature where it actually helps.

Here’s the short version of each feature, before we build with them:

  • View Transitions — a new <ViewTransition> component that hooks into the browser’s native View Transition API so elements animate in, out, and across the page without a separate animation library.
  • Fragment Refs — you can now pass a ref to <Fragment> and get a handle on a group of DOM children, even when there’s no wrapper element to attach the ref to.
  • browser() — a function that lets a component explicitly opt out of server rendering when server rendering doesn’t make sense for it (think: components that depend entirely on window or browser-only APIs).
  • Trusted Types support — React can now be configured to satisfy a page’s Trusted Types Content Security Policy, which locks down how strings get turned into DOM, script, or URL content.
  • Context in Server Components — Server Components can now read Context values, closing a gap that previously forced prop drilling across the server/client boundary.

According to the React team, “React 19.3 is now available! This release makes View Transitions and Fragment Refs stable, and adds browser(), Trusted Types support, and Context in Server Components” (React, official announcement). The word “stable” is doing real work there — both View Transitions and Fragment Refs existed in earlier experimental builds, and 19.3 is the release where they become safe to use in production code.

Prerequisites and versions

Confirm these before you start. Version mismatches are the single biggest cause of “the tutorial doesn’t work” reports on React upgrade threads, so don’t skip this.

RequirementMinimum versionNotes
Node.js20.x or later (22.x recommended)React 19.3’s tooling and Vite both expect a current LTS Node release
npm10.x or laterComes bundled with recent Node installs
react / react-dom19.3.0Released September 9, 2026 — check with npm ls react
Vite6.x or laterUsed as the build tool in this tutorial; CRA is no longer maintained
TypeScript (optional)5.6 or laterNeeded if you want typed Fragment Refs and ViewTransition props
Browser for testingChrome 126+ or Edge 126+View Transitions API support is still inconsistent across engines — see the browser support section below
Code editorVS Code or CursorEither works; this tutorial doesn’t depend on editor-specific tooling

You’ll also want basic comfort with hooks (useState, useEffect, useRef) and the command line. If you’re starting from React 18, read the upgrade notes section below before jumping into the new APIs — a couple of 19.0 changes (the new JSX transform requirements, ref-as-prop handling) will bite you if you skip straight to 19.3 features.

Step 1: Scaffold a fresh React 19.3 project

Starting clean is the fastest way to confirm your toolchain works before you touch any migration code. Use Vite’s React-TS template:

npm create vite@latest react-193-tutorial -- --template react-ts
cd react-193-tutorial
npm install
npm install [email protected] [email protected]
npm run dev

Open the local dev server URL Vite prints (typically http://localhost:5173) and confirm the default counter app renders. Then check your installed version:

npm ls react react-dom
# Expect:
# [email protected]
# [email protected]

If npm resolves an older patch version, pin it explicitly in package.json with "react": "19.3.0" and "react-dom": "19.3.0", delete node_modules and package-lock.json, then reinstall. React’s own 19.3 release notes confirm the package is distributed through npm the same way as prior 19.x releases, so there’s no special install flow or separate CLI.

Step 2: Upgrading an existing React 18 or 19.x app

If you’re migrating a real app rather than starting fresh, upgrade in two hops: React 18 to 19.0 first, then 19.0 to 19.3. Jumping straight from 18 to 19.3 technically works for most apps, but debugging is easier if you isolate the 19.0 breaking changes from the 19.3 feature additions.

# Step A: land on React 18.3 first (helps surface deprecation warnings)
npm install [email protected] [email protected]

# Step B: run the official codemod to catch breaking API usage
npx codemod@latest react/19/migration-recipe

# Step C: move to the current 19.3 release
npm install [email protected] [email protected]

Watch for three common 19.0 breaking changes the codemod catches: removed propTypes and defaultProps support on function components, the new requirement that ref is passed as a regular prop instead of through forwardRef in most cases, and changes to how react-dom/test-utils is imported. None of these are new to 19.3, but they’ll surface as errors the first time you touch an old codebase.

Once the codemod passes and the app builds clean on 19.0 or 19.2, bumping to 19.3.0 should not introduce new breaking changes — it’s additive. Run your existing test suite after each hop, not just at the end, so a regression doesn’t get buried under three version bumps’ worth of diffs.

Step 3: Build the project shell

The working project for this tutorial is a small “task board” app: a card list where cards animate when reordered (View Transitions), a toolbar that measures a cluster of buttons without a wrapper div (Fragment Refs), a widget that explicitly skips server rendering (browser()), and a sanitized note field (Trusted Types). It’s intentionally compact so every new API gets exercised without a pile of boilerplate.

Replace the contents of src/App.tsx with this shell, then we’ll add each feature on top of it:

import { useState } from "react";
import "./App.css";

type Task = { id: string; title: string; done: boolean };

const initialTasks: Task[] = [
  { id: "t1", title: "Draft release notes", done: false },
  { id: "t2", title: "Update Storybook", done: false },
  { id: "t3", title: "Review PR #482", done: false },
];

export default function App() {
  const [tasks, setTasks] = useState(initialTasks);

  return (
    

Task Board

    {tasks.map((task) => (
  • {task.title}
  • ))}
); }

Confirm this renders a plain list before moving on. If it doesn’t, fix that first — layering new APIs on top of a broken base is how debugging sessions spiral.

Step 4: Animate list reordering with <ViewTransition>

The new <ViewTransition> component wraps elements so React can animate them as they enter, exit, move, or resize, using the browser’s native View Transition API under the hood. Per the ViewTransition reference docs, “<ViewTransition> lets you animate a component tree with Transitions and Suspense.” That’s the key constraint: it animates changes that happen inside a React Transition (via startTransition or actions), not every state update automatically.

Add a “move to top” button and wrap the list in <ViewTransition>:

import { useState, useTransition, unstable_ViewTransition as ViewTransition } from "react";
import "./App.css";

type Task = { id: string; title: string; done: boolean };

const initialTasks: Task[] = [
  { id: "t1", title: "Draft release notes", done: false },
  { id: "t2", title: "Update Storybook", done: false },
  { id: "t3", title: "Review PR #482", done: false },
];

export default function App() {
  const [tasks, setTasks] = useState(initialTasks);
  const [isPending, startTransition] = useTransition();

  function moveToTop(id: string) {
    startTransition(() => {
      setTasks((current) => {
        const target = current.find((t) => t.id === id)!;
        const rest = current.filter((t) => t.id !== id);
        return [target, ...rest];
      });
    });
  }

  return (
    

Task Board

    {tasks.map((task) => (
  • {task.title}
  • ))}
); }

Click “Move to top” on the second or third item and the list should visibly slide into its new order instead of snapping. If you don’t see animation, check two things: the state update must happen inside startTransition, and your browser must support the View Transition API (see the browser support table in the troubleshooting section).

Output example — console and DOM behavior you should see on a supporting browser:

// No console errors on click
// DOM: the clicked <li> animates from its old position to the top
// over ~250ms using the browser's default view-transition easing

Step 5: Group DOM elements with Fragment Refs

Before 19.3, if you needed a single ref over a group of sibling elements, you had to wrap them in a <div> just to have somewhere to attach the ref — even when that wrapper broke your CSS grid or flex layout. Fragment Refs remove that workaround. As React’s docs put it, “Fragment Refs solve these problems by providing a limited set of commonly used DOM methods that work with any React component, regardless of what it renders” (React 19.3 release notes).

Add a toolbar of three buttons, grouped with a Fragment ref so you can measure their combined bounding box without an extra wrapper element:

import { Fragment, useRef, useState } from "react";

function Toolbar() {
  const toolbarRef = useRef | null>(null);
  const [width, setWidth] = useState(null);

  function measure() {
    const rect = toolbarRef.current?.getBoundingClientRect();
    if (rect) setWidth(Math.round(rect.width));
  }

  return (
    
{width !== null &&

Combined toolbar width: {width}px

}
); }

The React team’s own framing matters here: “Pass a ref to <Fragment> to work with its DOM children as a group, without adding a wrapper element” (React, official announcement). The ref gives you a subset of DOM methods — things like getBoundingClientRect, focus, and querying — rather than a full element reference, because there’s no single underlying DOM node to hand back.

Drop <Toolbar /> into App.tsx above the task list and click “Measure toolbar.” You should get a pixel width back without any wrapper <div> showing up in your DevTools Elements panel.

Step 6: Opt a component out of server rendering with browser()

browser() is aimed at components that genuinely can’t do anything useful on the server — things built entirely around window, localStorage, canvas APIs, or other browser-only globals. Instead of guarding every line with typeof window !== "undefined" checks, you call browser() to tell React this component should skip server rendering outright and render only on the client.

import { browser } from "react";
import { useEffect, useState } from "react";

function LocalTaskCount() {
  browser();

  const [count, setCount] = useState(0);

  useEffect(() => {
    const stored = window.localStorage.getItem("task-count");
    setCount(stored ? Number(stored) : 0);
  }, []);

  return 

Tasks stored locally: {count}

; }

In a client-only Vite app like the one we scaffolded, you won’t see a visible difference since there’s no server render happening in the first place. The practical use case shows up once you add a server-rendering framework (Next.js App Router or a custom React Server Components setup): without browser(), a component touching window at module scope throws during the server pass. With it, React knows to skip straight to client rendering for that subtree instead of crashing the whole page.

If you want to see the effect in context, the companion piece to this is Context support in Server Components — also new in 19.3 — which means a Server Component can now read a Context value set higher in the tree instead of needing that value passed down as a prop through every layer. Combined with browser(), you get a cleaner split: Server Components read shared Context for data, client-only components opt out cleanly when they need the DOM.

Step 7: Satisfy a Trusted Types CSP

Trusted Types is a browser security feature (see MDN’s Trusted Types API docs) that blocks raw strings from being assigned to sensitive DOM sinks like innerHTML unless they’ve been wrapped in a policy-approved object. Pages that enforce a strict Trusted Types Content-Security-Policy would previously break React apps that rely on any internal string-to-DOM assignment. React 19.3 adds support for configuring a policy React itself will use, so your app can run under a Trusted Types CSP without forking React internals.

Add a note field to the task board that renders user-supplied text, and configure a Trusted Types policy around it:

// trustedTypesSetup.ts — import this once, before React renders
if (typeof window !== "undefined" && window.trustedTypes) {
  window.trustedTypes.createPolicy("react-app-policy", {
    createHTML: (input: string) => input,
    createScript: () => "",
    createScriptURL: () => "",
  });
}
# To test this locally, serve the app with a strict CSP header:
Content-Security-Policy: require-trusted-types-for 'script'; trusted-types react-app-policy

Import trustedTypesSetup.ts at the top of main.tsx, before ReactDOM.createRoot runs. Without it, a page serving that CSP header will throw a TrustedTypesPolicyViolation the moment React tries to touch the DOM. With the policy registered, React’s internal DOM writes route through it and the page loads clean. This matters most for teams in regulated industries or anyone running a strict CSP as part of a security hardening pass — it’s not something a typical hobby project needs, but it’s the kind of thing that blocks an entire migration if you hit it unprepared.

Step 8: Wire it all together in the task board

With all four pieces built, assemble the final App.tsx:

import { Fragment, useRef, useState, useTransition, unstable_ViewTransition as ViewTransition } from "react";
import "./App.css";

type Task = { id: string; title: string; done: boolean };

const initialTasks: Task[] = [
  { id: "t1", title: "Draft release notes", done: false },
  { id: "t2", title: "Update Storybook", done: false },
  { id: "t3", title: "Review PR #482", done: false },
];

function Toolbar() {
  const toolbarRef = useRef | null>(null);
  const [width, setWidth] = useState(null);

  function measure() {
    const rect = toolbarRef.current?.getBoundingClientRect();
    if (rect) setWidth(Math.round(rect.width));
  }

  return (
    
{width !== null &&

Combined toolbar width: {width}px

}
); } export default function App() { const [tasks, setTasks] = useState(initialTasks); const [isPending, startTransition] = useTransition(); const [note, setNote] = useState(""); function moveToTop(id: string) { startTransition(() => { setTasks((current) => { const target = current.find((t) => t.id === id)!; const rest = current.filter((t) => t.id !== id); return [target, ...rest]; }); }); } return (

Task Board

    {tasks.map((task) => (
  • {task.title}
  • ))}