Lesson 30 of 35
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.
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:
- 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. - Fix the real violations. Wrap every
dangerouslySetInnerHTMLcaller in a Trusted Types policy. Replace any third-party library'sinnerHTMLuse (chart libraries, markdown renderers) with a policy or a sanitized call. - 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,
dangerouslySetInnerHTMLrequires its value to be aTrustedHTMLproduced 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
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.What does a ref attached to a Fragment actually give you?
3.What does the browser's Trusted Types feature do, and why does a React 19.3 app benefit from built-in support?
4.After enabling
require-trusted-types-for 'script'in your CSP, what doesdangerouslySetInnerHTML={{ __html: userBio }}do ifuserBiois a plain string?
4 left to answer
Discussion
Sign in to postNo comments yet. Be the first to say something.