DEV Community

Cover image for Let's learn React 19.3 by building a Bug Zoo
The Release Desk
The Release Desk

Posted on AI-assisted

Let's learn React 19.3 by building a Bug Zoo

React 19.3 shipped on September 9 (19.3.1 followed on October 14). The release notes are long, and reading them is not the same as getting them.

So here's how we learn new stuff at The Release Desk: we build one small project and use every new concept in it for real. This time it's a zoo of famous bugs.

First, a quick hello to each new feature. Then we open the zoo.

Part 1: Meet the five new tricks

1. <ViewTransition>: things move instead of jump

Wrap an element, and React animates it when it appears, disappears or changes. It uses the browser's built-in View Transition API, now stable in React.

<ViewTransition><Card /></ViewTransition>
Enter fullscreen mode Exit fullscreen mode

You'll want it when: a list item opens into a detail page, or a modal pops in.

2. addTransitionType: tell the animation why it's happening

Same state change, different animation. Moving "next" slides left; moving "previous" slides right.

startTransition(() => {
  addTransitionType('next');
  setPage(p => p + 1);
});
Enter fullscreen mode Exit fullscreen mode

You'll want it when: you have carousels, steppers or tabs.

3. Suspense + ViewTransition: loading that doesn't flicker

Wrap a <Suspense> boundary and the skeleton β†’ content swap becomes a smooth fade, not a pop.

<ViewTransition><Suspense fallback={<Skeleton />}>…</Suspense></ViewTransition>
Enter fullscreen mode Exit fullscreen mode

You'll want it when: any data loads after a click.

4. Fragment refs: a ref on a group, no wrapper <div>

Put a ref on a <Fragment> and you can observe, focus or listen to all its children at once. That includes components that don't accept a ref.

<Fragment ref={ref}>{children}</Fragment>
Enter fullscreen mode Exit fullscreen mode

You'll want it when: you'd otherwise add a wrapper <div> that breaks your grid or flex layout.

5. use(browser()): "don't render me on the server"

One line opts a component out of server rendering. The server shows the Suspense fallback; the browser renders the real thing.

use(browser());
Enter fullscreen mode Exit fullscreen mode

You'll want it when: the component needs localStorage, window or the user's time zone. Goodbye, mounted + useEffect trick.

Part 2: Let's understand them with a simple project

Meet the Bug Zoo. Eight famous bugs live in it: Off-by-One, Race Condition, Stale Closure, Hydration Mismatch… and the Heisenbug, which only shows up when nobody's looking.

Each new feature does one job in the zoo.

The Bug Zoo demo: a card morphs into a page, pages slide left and right, field notes fade in
Recorded in Edge from the real app. Avatar morph, direction-aware slides, skeleton β†’ content.

Setup is just Vite plus:

npm i react@19.3.1 react-dom@19.3.1
Enter fullscreen mode Exit fullscreen mode

πŸ› Opening a bug: the card morphs into its page (tricks 1 + 2)

Give the avatar the same name on the grid and on the bug's page. React morphs it from one spot to the other.

// On the zoo grid
<ViewTransition name={`bug-${bug.id}`} share="auto" default="none">
  <span className="avatar">{bug.emoji}</span>
</ViewTransition>

// On the bug's page: same name, bigger avatar
<ViewTransition name={`bug-${bug.id}`} share="auto" default="none">
  <span className="avatar big">{bug.emoji}</span>
</ViewTransition>
Enter fullscreen mode Exit fullscreen mode

Every navigation goes through one function, so it's always a Transition, and always typed:

function go(id, type) {
  startTransition(() => {
    addTransitionType(type); // 'open' | 'close' | 'next' | 'previous'
    setOpenId(id);
  });
}
Enter fullscreen mode Exit fullscreen mode

How it works: React snapshots the old screen, swaps the DOM, then animates between the two. The catch: only Transitions animate. A plain setOpenId(id) just jumps.

🐜 Next / Previous: direction-aware slides (trick 2)

<ViewTransition
  key={bug.id}
  enter={{ next: 'from-right', previous: 'from-left', default: 'none' }}
  exit={{ next: 'to-left', previous: 'to-right', default: 'none' }}
>
  <article>…</article>
</ViewTransition>
Enter fullscreen mode Exit fullscreen mode

Those strings are plain CSS classes:

::view-transition-new(.from-right) { animation: 280ms 80ms ease-out both slide-from-right; }
::view-transition-old(.to-left)    { animation: 180ms ease-in both slide-to-left; }
@keyframes slide-from-right { from { transform: translateX(120px); opacity: 0; } }
@keyframes slide-to-left    { to   { transform: translateX(-120px); opacity: 0; } }
Enter fullscreen mode Exit fullscreen mode

A trap we hit: our first slide looked like a double exposure. The named avatar was animating on its own instead of riding along with its page. default="none" limits it to the morph only, so now it slides with its page.

πŸͺ² Field notes: calmer loading (trick 3)

Each bug's notes come from a (fake, 900 ms) API:

<ViewTransition update="auto" default="none">
  <Suspense fallback={<NotesSkeleton />}>
    <FieldNotes id={bug.id} />
  </Suspense>
</ViewTransition>
Enter fullscreen mode Exit fullscreen mode
function FieldNotes({ id }) {
  const notes = use(fetchNotes(id)); // cached promise
  return <dl>…</dl>;
}
Enter fullscreen mode Exit fullscreen mode

update="auto" default="none" follows React's own rule of thumb:

  • the skeleton appears instantly (no fade)
  • skeleton β†’ content animates
  • second visit (cached) appears instantly

The Race Condition page with the skeleton still loading

πŸ•·οΈ The "Spotted 4/8" counter: Fragment refs (trick 4)

The header counts the bugs you've scrolled past. That needs an IntersectionObserver on every card, but BugCard doesn't take a ref, and a wrapper div would break the CSS grid.

function Spotter({ onSpot, children }) {
  const ref = useRef(null);
  useEffect(() => {
    const fragment = ref.current;
    const io = new IntersectionObserver((entries) => {
      for (const e of entries) if (e.isIntersecting) onSpot(e.target.dataset.bug);
    }, { threshold: 0.9 });
    fragment.observeUsing(io); // observes every first-level child
    return () => fragment.unobserveUsing(io);
  }, []);
  return <Fragment ref={ref}>{children}</Fragment>;
}
Enter fullscreen mode Exit fullscreen mode

In our test the counter went from 4/8 to 8/8 after scrolling down. The same FragmentInstance can also focus(), addEventListener and scrollIntoView across the group.

πŸ¦— The Heisenbug: use(browser()) (trick 5)

The Heisenbug's page shows your local time and time zone. A server can't know those.

import { browser } from 'react-dom';

function Sighting() {
  use(browser()); // suspends on the server, not in the browser
  const zone = Intl.DateTimeFormat().resolvedOptions().timeZone;
  const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
  return <p>πŸ¦— Spotted at {time} ({zone}), only in your browser.</p>;
}

<Suspense fallback={<p>πŸ‘€ The server looked. The Heisenbug vanished.</p>}>
  <Sighting />
</Suspense>
Enter fullscreen mode Exit fullscreen mode

We server-rendered that page with renderToString. The real HTML:

<p class="sighting">πŸ‘€ The server looked. The Heisenbug vanished.</p>
Enter fullscreen mode Exit fullscreen mode

In the browser, the same spot reads "Spotted at 12:09 pm (Asia/Calcutta)". The bug only exists when the browser is watching. Fitting.

The Heisenbug page in the browser, showing the local time zone

Bonus: use can sit behind an if, so "skip the server only when there's no default value" is a one-liner.

Before you ship it

Thing What happens
Update not in startTransition No animation. By design.
Older browser View Transitions are Baseline since Oct 2025. Older browsers just skip the animation; the app still works.
Tab in the background The browser skips the transition (we saw this while testing headless).
Users who prefer reduced motion Your job. We added a prefers-reduced-motion rule that turns the animations off.

Desk verdict: Upgrade to 19.3.1 if you're on React 19 already. Start with use(browser()): it deletes code. Add <ViewTransition> one screen at a time, and always behind startTransition.

Facts come from the React 19.3 release post and our own test run. Cover art: Jan van Kessel, Insects, Butterflies, and a Grasshopper (1664), The Met, CC0.

Top comments (0)