Topic 19.8
Ref as a Prop, Context as Provider and Other Improvements
In one line
React 19 removes everyday friction: function components receive ref as a normal prop, ref callbacks can return a cleanup function, <Context value> is a provider, hydration errors show one message with a diff, custom elements get properties instead of strings, and createRoot takes error reporting callbacks.
Think of it like this
A new version of your phone's operating system that doesn't add one big app but fixes a dozen small annoyances: the torch has a shortcut, the alarm has a snooze button, notifications are grouped. None is exciting alone, but every day is a little smoother. These React 19 changes are those fixes: less wrapper code, clearer errors, fewer surprises.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Ref
- An object or function that gives you access to a DOM node or a value that survives renders (Phase 8).
- forwardRef
- The pre-19 wrapper that let a function component receive a ref. No longer needed.
- Ref callback
- A function passed as
ref. React calls it with the DOM node when the node is attached. - Ref cleanup
- A function returned from a ref callback. React calls it when the node is removed.
- Hydration
- React attaching event handlers and state to HTML that the server already rendered.
- Hydration mismatch
- The server's HTML and the browser's first render disagree.
- Custom element
- An HTML tag you define yourself with JavaScript, also called a web component.
- onCaughtError / onUncaughtError
- Root options React calls when an error is caught by a boundary, or not caught at all.
Step by step
01Drop forwardRef
A component that hands its ref to an inner DOM node just takes ref from props. The forwardRef codemod does this automatically.
// Before
const TextField = forwardRef(function TextField({ label }, ref) {
return <label>{label} <input ref={ref} /></label>;
});
// React 19
function TextField({ label, ref }) {
return <label>{label} <input ref={ref} /></label>;
}02Return a cleanup from a ref callback
Setup and teardown for one DOM node now sit together, like in an effect. React calls the cleanup when the node is removed.
<div
ref={(node) => {
const chart = createChart(node);
return () => chart.destroy();
}}
/>03Provide context without .Provider
The context object itself is the provider component. Nested providers still override outer ones.
<ThemeContext value={theme}>
<Page />
</ThemeContext>04Report errors from the root
Pass callbacks when creating the root. Each one gets the error and info.componentStack.
Try it yourself
- 1
Toggle the box back
In the first example, click Toggle box twice by hand. Predict the logs: a cleanup, then a new
ref attached. Each mount gets a fresh observer. - 2
Remove the inner provider
In the context example, delete the inner
<ThemeContext value="sepia">(keep its<Badge />). Predict what both badges show before and after Switch. - 3
Make the clock match
In the error example, render
10:30on both sides. Predict whichrecoverableline disappears. (This is why hydration-safe code avoids reading the clock during render; Topic 20.6.)
Code & diagrams
`TextField` passes the `ref` prop straight to its input. When the box is removed React calls the returned cleanup, not the callback with `null`.
Preview
Console
Nothing logged yet.Expected console output
ref attached: box
focused: true
ref cleanup: stop observingNo `.Provider`. The inner provider overrides the outer one, so the second badge always sees `sepia`. Both badges re-render on Switch because their parent `App` re-rendered.
Preview
Console
Nothing logged yet.Expected console output
Badge sees dark
Badge sees sepia
Badge sees light
Badge sees sepiaWith callbacks set, React calls them instead of logging to the console itself. The hydration mismatch is recoverable: React threw away the server text and rendered the client's. (The first line of each message is logged here; a real reporter sends the whole error.)
Preview
Console
Nothing logged yet.Expected console output
caught: Widget crashed | stack has component: true
recoverable: Hydration failed because the server rendered text didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used: | stack has component: true
page now: Rendered at 10:31Uncaught Error: Hydration failed because the server rendered text didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
- A server/client branch `if (typeof window !== 'undefined')`.
- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.
- Date formatting in a user's locale which doesn't match the server.
- External changing data without sending a snapshot of it along with the HTML.
- Invalid HTML tag nesting.
It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
https://react.dev/link/hydration-mismatch
<Clock time="10:31">
<p>
+ 10:31
- 10:30`stars` exists as a property on the element, so React set it as a number. `label` has no property, so it became an attribute. React 18 would have set `stars="3"` as a string attribute.
Preview
Console
Nothing logged yet.Expected console output
property stars: 3 (number)
attribute stars: null
attribute label: Quality
text: ★★★ (Quality)Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
An implicit return from a ref callback
Write ref={(node) => (box = node)} in TypeScript with React 19 types.
Break #2
Remove the root error callbacks
In the error example, create the hydration root without onRecoverableError.
Myth vs fact
Myth
forwardRef is broken in React 19.
Fact
It works as before. It's just unnecessary for function components now.
Myth
<Context.Provider> was removed.
Fact
It still works in 19. <Context value> is the new, shorter form; .Provider will be deprecated later.
Myth
React doesn't support web components.
Fact
React 19 fully supports custom elements: properties are set with real values and custom events can be listened to with on props.
Myth
A ref callback still gets called with null on unmount.
Fact
Only if it doesn't return a cleanup function. With a cleanup, React calls the cleanup instead.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Because
refis a regular prop,React.memoand spreading props pass it along like any other prop. Libraries that inspectedelement.refmust readelement.props.ref; the old getter warnsAccessing element.ref was removed in React 19. ref is now a regular prop. - ▸
Ref callbacks whose identity changes every render (inline arrows) are detached and re-attached on every commit: the cleanup and setup both run. That's harmless for focus code but wasteful for heavy setup; use
useCallbackor a stable function for those. - ▸
onUncaughtErroris the place to show a global "Something went wrong, reload" screen: React has already unmounted the root when it runs, so render a fallback with plain DOM or a new root. - ▸
React 19's custom element support means server rendering outputs primitive props as attributes, while the client sets properties during hydration. Design web components to accept both an attribute and a property for primitive values.
Remember this
- 1
**
refas a prop. A ref** (Phase 8) lets a parent reach a DOM node, for example to focus an input. Before 19,refwas special: a function component couldn't receive it unless wrapped inforwardRef((props, ref) => ...). In React 19refis an ordinary prop for function components:function TextField({ label, ref }) { return <input ref={ref} /> }.forwardRefstill works (for libraries supporting older React) and will be deprecated later. Class components still treatrefspecially: it points to the class instance. Readingelement.refon a JSX element now warns, because it's justelement.props.ref. - 2
Ref callback cleanup. A ref callback is a function passed as
ref: React calls it with the DOM node when it's attached. Before 19 React called it again withnullwhen the node was removed, so setup and teardown lived in two branches. In 19 the callback may return a cleanup function, like an effect: React calls the cleanup when the node is detached (or the callback changes), and then does not call the callback withnull. Great for observers, third-party widgets and event listeners on a specific node. Because a returned value now means something, TypeScript rejects implicit returns such asref={(n) => (inputRef = n)}: use a block body. - 3
**
<Context>as a provider.** Instead of<ThemeContext.Provider value="dark">you write<ThemeContext value="dark">(Topic 9.2). Read it withuseContext(ThemeContext)oruse(ThemeContext)..Providerstill works and is planned for deprecation; a codemod converts it. - 4
Better hydration errors. Hydration (Topic 20.6) is React attaching to HTML that the server already rendered. If the server HTML and the first client render differ (a time, a random number,
typeof windowchecks), React 18 printed several vague errors. React 19 logs one error that lists the likely causes and shows a diff: lines with+are what the client rendered,-what the server sent. React then re-renders that part on the client. React 19 also tolerates unexpected tags that browser extensions or third-party scripts insert in<head>and<body>, instead of failing hydration. - 5
Custom elements. A custom element (web component) is an HTML tag defined with JavaScript:
customElements.define("star-rating", StarRating). React 18 passed every prop as a string attribute, so arrays, objects and numbers arrived as"[object Object]". React 19 checks whether the element has a property with that name: if it does, React sets the property with the real value (el.stars = 3), otherwise an attribute. Booleanstruebecome empty attributes, andon+ name props attach event listeners for custom events. React now passes all of the Custom Elements Everywhere tests. - 6
Error reporting options.
createRoot(container, options)andhydrateRootacceptonCaughtError(error, info)(an error boundary caught it),onUncaughtError(error, info)(no boundary: the root unmounted) andonRecoverableError(error, info)(React recovered on its own, for example after a hydration mismatch).info.componentStacksays which components were rendering. Send them to your error service (Sentry and others) instead of patchingconsole.error. Also new in 19: an error caught by a boundary is reported once (not re-thrown and logged twice as in 18). Other small 19 improvements:useDeferredValue(value, initialValue)takes an initial value, and<StrictMode>reuses memoised results between its double renders.
Explain it without notes
How do you pass a ref to a function component in React 19, and what changed from React 18?
What do ref cleanup functions add, and how do they change the null call?
What do onCaughtError, onUncaughtError and onRecoverableError receive and when are they called?
How does React 19 handle props on custom elements?
Practice
Convert a forwardRef SearchBox to React 19 and focus it from a parent button.
Write a ref callback that adds a keydown listener to a div and removes it in the cleanup.
Trade-offs
- ↔
Dropping
forwardRefsimplifies components, but a library that must support React 18 still needs it; keepforwardRefin shared packages until you drop 18. - ↔
Root error callbacks centralise reporting, but they replace React's default console logging; log to the console yourself in development so you still see errors locally.
Done when you can
I can accept
refas a prop in a function component.I can return a cleanup function from a ref callback.
I can provide context with
<Context value>.I can read a React 19 hydration error diff.
I know how React 19 passes props to custom elements.
I can wire
onCaughtError,onUncaughtErrorandonRecoverableErrorto an error service.