AniUI Academy

Fragment Refs and Trusted Types

Two small React 19.3 wins worth knowing — attaching a single ref to many sibling elements through a Fragment, and the first-class Trusted Types support that lets you ship a strict CSP policy without the usual dangerouslySetInnerHTML escape hatches.

8 min read

Two small React 19.3 additions live in the same lesson because they're both narrow, easy to use once you've seen them, and quietly remove long-standing friction. Fragment refs replace a tired wrapper-div pattern; Trusted Types support lets you turn on a stricter Content Security Policy without React itself getting in the way.

Fragment refs

The problem Fragment refs solve

Before React 19.3, a ref attached to anything other than a single DOM element was awkward. The most common paper cut was observing a group of sibling elements with an IntersectionObserver or ResizeObserver:

// The old shape — wrap the siblings in a div you don't actually want.
function GalleryRow({ photos }) {
  const ref = useRef(null);
  useEffect(() => {
    const observer = new IntersectionObserver(onVisible, { threshold: 0.5 });
    ref.current?.childNodes.forEach((n) => observer.observe(n));
    return () => observer.disconnect();
  });
  return (
    <div ref={ref} className="row">
      {photos.map((p) => <img key={p.id} src={p.src} alt={p.alt} />)}
    </div>
  );
}

That wrapper div is the entire bug surface. Any flex or grid layout expecting the images as direct children of .row now has an extra element in the way. Teams have shipped display: contents on the wrapper for years as a half-fix, which breaks accessibility semantics and some browsers' layout fast paths.

What a Fragment ref gives you

React 19.3 lets you attach a ref directly to a Fragment. The ref doesn't point at a DOM node — Fragments don't render one — it points at a small handle whose job is to let you treat the group of children as one target for the operations that used to need a wrapper.

import { Fragment, useEffect, useRef } from "react";
 
function GalleryRow({ photos }) {
  const ref = useRef(null);
  useEffect(() => {
    const observer = new IntersectionObserver(onVisible, { threshold: 0.5 });
    ref.current?.observeUsing(observer); // attaches to every child
    return () => ref.current?.unobserveUsing(observer);
  }, []);
  return (
    <Fragment ref={ref}>
      {photos.map((p) => <img key={p.id} src={p.src} alt={p.alt} />)}
    </Fragment>
  );
}

observeUsing(observer) attaches the given IntersectionObserver, ResizeObserver, or MutationObserver to every child of the Fragment — so one line replaces the childNodes.forEach(...) dance. unobserveUsing is the inverse. There are matching helpers for focus and scroll that iterate across the siblings the same way.

When Fragment refs are the right reach

Three shapes come up most:

  • Observing a list of siblings with a browser Observer API, without introducing a wrapper the layout doesn't want.
  • Measuring the combined bounding box of a group of siblings (for tooltips, selection UIs, drag previews). The handle exposes a way to iterate over the children's rects.
  • Scrolling or focusing the first/last of a group without needing a ref on each individual element.

If a single DOM node would have worked anyway — a card with one image, a form with one field — keep writing a plain element ref. Fragment refs are for actual groups of siblings the layout wants kept flat.

Trusted Types support

The problem Trusted Types solves

The browser's Trusted Types feature is a Content Security Policy mechanism that closes the biggest remaining XSS footgun: assigning a raw string to a dangerous DOM sink.

Before Trusted Types, this line is the shape of practically every stored XSS bug in the wild:

element.innerHTML = untrustedString;

With require-trusted-types-for 'script' in the page's CSP, that assignment stops working. The browser requires the right-hand side to be a TrustedHTML value, produced by a named policy whose job is to sanitize the input. A raw string is rejected, synchronously, by the browser — there is no way for a careless innerHTML write to turn user input into executable DOM.

What React 19.3 contributes

React itself writes to innerHTML in a few places — the dangerouslySetInnerHTML prop is the obvious one, but the reconciler also uses innerHTML internally for some fast paths. Before 19.3, turning on require-trusted-types-for 'script' caused those writes to trip the policy and fail, so teams would carve exceptions for React or avoid the CSP rule entirely.

React 19.3 creates trusted values for its own DOM writes through a built-in policy. The practical effect is that you can ship a CSP like:

Content-Security-Policy: require-trusted-types-for 'script'; trusted-types react-app dompurify

…and React's own innerHTML writes go through without false-positive violations.

What this means for your app code

For normal components — JSX, props, children — nothing changes. React sanitizes text content automatically; that was already safe.

The place you feel the policy is dangerouslySetInnerHTML. Under a Trusted Types CSP, its __html value has to be a TrustedHTML, not a plain string:

// Before — plain string, works without a Trusted Types CSP, rejected under one.
<div dangerouslySetInnerHTML={{ __html: userBio }} />
// After — sanitized through a Trusted Types policy, passes.
import DOMPurify from "dompurify";
 
const policy = window.trustedTypes?.createPolicy("dompurify", {
  createHTML: (input) => DOMPurify.sanitize(input),
});
 
function Bio({ userBio }) {
  const trusted = policy?.createHTML(userBio) ?? userBio;
  return <div dangerouslySetInnerHTML={{ __html: trusted }} />;
}

DOMPurify is the usual sanitizer; any library that returns clean HTML can be wrapped in a policy the same way. The policy's name ("dompurify" above) has to appear in the CSP's trusted-types directive — the browser refuses to load unregistered policies.

How to actually turn this on

A reasonable rollout for an existing app:

  1. Report-only first. Set the header to Content-Security-Policy-Report-Only: require-trusted-types-for 'script' so violations are logged but not blocked. Deploy and watch the reports for a week.
  2. Fix the real violations. Wrap every dangerouslySetInnerHTML caller in a Trusted Types policy. Replace any third-party library's innerHTML use (chart libraries, markdown renderers) with a policy or a sanitized call.
  3. Flip to enforcing mode — same header without -Report-Only. The same violations that were logged now become hard errors in the browser.

Trusted Types isn't a free lunch — the app does have to be a little thoughtful about sanitization — but once the policy is on, the "stored XSS through user bio" shape of bug is physically impossible, not just unlikely.

What to remember

  • Fragment refs attach a ref to a Fragment and give you a handle whose observeUsing(observer) / unobserveUsing(observer) apply a browser Observer to every child. Focus and scroll helpers iterate the same way.
  • Use Fragment refs when you need to treat a group of siblings as one observation target without introducing a wrapper the layout doesn't want. A single element with one ref is still the right tool for a single element.
  • Trusted Types is a browser CSP feature that forbids raw strings from reaching dangerous DOM sinks. React 19.3 handles its own internal writes through a built-in policy, so enabling require-trusted-types-for 'script' doesn't break React itself.
  • Under a Trusted Types CSP, dangerouslySetInnerHTML requires its value to be a TrustedHTML produced by a named policy (commonly a DOMPurify-backed one). Normal JSX text content is unaffected.
  • Roll the policy out in Report-Only mode first, fix the real violations, then flip to enforcing. That path produces a strict, auditable XSS story without one big-bang breakage.

Check yourself

4 questions · pass 3/4 to unlock Suspense for Data Fetching

up to 50
  1. 1.Before Fragment refs, what was the standard workaround when you needed to observe or measure a group of sibling elements that had no natural wrapper?

  2. 2.What does a ref attached to a Fragment actually give you?

  3. 3.What does the browser's Trusted Types feature do, and why does a React 19.3 app benefit from built-in support?

  4. 4.After enabling require-trusted-types-for 'script' in your CSP, what does dangerouslySetInnerHTML={{ __html: userBio }} do if userBio is a plain string?

4 left to answer

Discussion

Sign in to post
Sign in to join the conversation.Sign in

No comments yet. Be the first to say something.