Skip to content

Conversation

@serhalp
Copy link
Member

@serhalp serhalp commented Jun 5, 2025

Summary

This adds support for React 19 to all Gatsby packages, while maintaining support for React 18.

This is not a breaking change.

All packages' peer dependencies on react and react-dom have been extended from ^18.0.0 to ^18.0.0 || ^19.0.0.

All existing stable Gatsby functionality is intended to work with React 19.

Upgrade Guide

To upgrade to React 19, first upgrade gatsby and all your gatsby-* dependencies to the latest version. Then, follow the React 19 upgrade guide. No other changes are required.

Please note:

New features

Gatsby now supports React 19's new root error callbacks.

Users can export onCaughtError and onUncaughtError from their gatsby-browser.js to handle errors caught by error boundaries and uncaught errors respectively:

// gatsby-browser.js

export const onCaughtError = ({ error, errorInfo }) => {
  // e.g. send to an error tracking service
  myErrorTracker.reportError(error, { extra: errorInfo })
}

export const onUncaughtError = ({ error, errorInfo }) => {
  // e.g. send to an error tracking service
  myErrorTracker.captureException(error, { extra: errorInfo })
}

In development, these errors also appear in Gatsby's Fast Refresh error overlay. These callbacks are only invoked in React 19.

Implementation

This PR configures CI to run the existing development-runtime and production-runtime e2e test suites against both React 18 and 19.

fix(gatsby-plugin-image): work around a regression in React 19 core

There is an undocumented change in behaviour in React 19: facebook/react#31660. Basically, in previous versions, an unchanged dangerouslySetInnerHTML.__html would not result in a re-render, but in React 19 referential equality of the dangerouslySetInnerHTML object is used instead.

The gatsby-plugin-image implementation fundamentally depends on the previous behaviour, so that our own innerHTML updates do not get clobbered by a reset to this dangerouslySetInnerHTML placeholer value.

As a workaround, this commit memoizes the object with useMemo().

This is safe for React 18 as well.

fix: replace usages of __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED 😶

There was one use of React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED?.ReactDebugCurrentFrame?.getCurrentStack(). React 19 introduced React.captureOwnerStack() which we'll now use instead if available.

fix: avoid conflicts between Gatsby Head API and new React 19 document metadata feature

There are two conflicts here:

  1. React 19 no longer allows rendering a second <html> or <body> element anywhere in the tree.
  2. React 19 hoists document metadata tags (<title>, <meta>, etc.) automatically into the <head>, with similar semantics as Gatsby's Head API.

These both conflict with Gatsby's Head API implementation, which renders all these elements in a hidden <div>, which it later finds to extract, merge, and apply attributes on the actual DOM nodes.

We work around this with two techniques:

  • Tags that ultimately belong in <head> are wrapped in an <svg> tag 😬, which prevents React 19's new mechanism from hoisting them, letting Gatsby process them as before.
  • We monkey-patch React.createElement (when using React 19 only) to intercept <html> and <body> elements within the Gatsby Head API context, replacing them with <div data-original-tag="html|body"> stand-ins. This allows Gatsby to extract attributes from these elements without triggering React 19's restrictions.

Future work

  • Expose less magical <HtmlAttributes> and <BodyAttributes> components (or some other API) to allow phasing out the nonstandard <html>/<body> magic?
  • Refactor Gatsby Head API implementation to use the native React document metadata functionality when available? or remove this part of the Gatsby Head API entirely in favour of users leveraging React's feature directly
  • Remove <svg> workaround by augmenting the React.createElement monkey-patch to check for <title>, <meta>, etc.? One workaround is better than two, maybe.

Experimental Partial Hydration incompatibility with React 19

Gatsby's experimental Partial Hydration feature has been flagged as experimental for about three years and relies on React's experimental react-server-dom-webpack package APIs that were substantially overhauled between React 18 and 19 as part of the RSC stabilization effort. Gatsby has been pinned to version 0.0.0-experimental-c8b778b7f-20220825 for years. The feature used experimental RSC APIs that no longer exist or are incompatible with React 19 for various reasons. Porting to React 19's stabilized RSC architecture would require substantial effort and the feature has seen limited adoption. It took massive research and iteration for other frameworks to reach RSC maturity and much of the effort was in underlying bundlers. It's very unlikely Gatsby will tackle this.

The problematic import (react-server-dom-webpack) was previously imported statically at the top of the module, causing React 19 projects to encounter import errors even when not using Partial Hydration. In this PR, we simply moved the import to a conditional async import that only loads when Partial Hydration is actually enabled via the (existing) opt-in flag.

This allows the majority of Gatsby projects to safely upgrade to React 19 while allowing projects that have opted in to Partial Hydration to continue using it by remaining on React 18 while still being able to receive Gatsby upgrades.

@gatsbot gatsbot bot added the status: triage needed Issue or pull request that need to be triaged and assigned to a reviewer label Jun 5, 2025
@serhalp serhalp force-pushed the feat/support-react-19 branch 2 times, most recently from d52b12d to 24546b0 Compare June 6, 2025 21:52
@serhalp serhalp force-pushed the feat/support-react-19 branch 5 times, most recently from 6ab44fb to 6680733 Compare August 5, 2025 19:55
@serhalp serhalp changed the title feat: wipwipwip feat: support React 19 Aug 5, 2025
@serhalp serhalp force-pushed the feat/support-react-19 branch 12 times, most recently from 3b417a1 to ccc2189 Compare August 8, 2025 12:53
@serhalp serhalp linked an issue Aug 8, 2025 that may be closed by this pull request
2 tasks
@serhalp serhalp force-pushed the feat/support-react-19 branch 4 times, most recently from 7f66511 to 3797d4d Compare November 14, 2025 14:52
@serhalp serhalp force-pushed the feat/support-react-19 branch 4 times, most recently from 1131ffe to e8b11fd Compare November 21, 2025 20:29
serhalp and others added 7 commits November 21, 2025 17:45
We support 20 and 22 now and this fail has been really annoying for me.
I can't imagine it's providing much value.
It was actually navigating to the 404 page, which happened to meet the expected conditions.
`title()` immediately resolves with the current title, whereas `.get()`
is deferred until the target element exists in the DOM:
https://docs.cypress.io/app/core-concepts/introduction-to-cypress#Implicit-Assertions.
There is an undocumented change in behaviour in React 19:
facebook/react#31660. Basically, in previous versions, an unchanged
`dangerouslySetInnerHTML.__html` would not result in a re-render, but in React 19 referential
equality of the `dangerouslySetInnerHTML` object is used instead.

This code fundamentally depends on the previous behaviour, so that our own `innerHTML` updates do
not get clobbered by a reset to this `dangerouslySetInnerHTML` placeholer value.

As a workaround, this commit memoizes the object with `useMemo()`.

This is safe for React 18 as well.
@serhalp serhalp force-pushed the feat/support-react-19 branch from e8b11fd to 85d9c8a Compare November 21, 2025 22:47

### Testing with different React versions

To test compatibility with different React versions, you can use the `upgrade-react.js` script to update the React version in a test directory, then run the tests:
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This already existed. I just added some docs and started (re!)using it in CI.

deduplication: `${path}/deduplication/`,
htmlAndBodyAttributes: `${path}/html-and-body-attributes/`,
headWithWrapRooElement: `${path}/head-with-wrap-root-element/`,
withoutHead: `${path}/without-head/`,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed this test happened to work because the 404 page happened to meet the right conditions (no Head()!

export default defineConfig({
e2e: {
baseUrl: "http://localhost:8000",
supportFile: false,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh hey, this must be why the support file wasn't working 😱

Comment on lines +18 to +29
## Test Cases

1. **Development Server**

- Verifies that the development server starts with React 19
- Tests React 19 state updates and hooks
- Tests error boundaries with React 19

2. **Production Build**
- Verifies that the production build completes successfully with React 19
- Checks for the existence of expected output files

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
## Test Cases
1. **Development Server**
- Verifies that the development server starts with React 19
- Tests React 19 state updates and hooks
- Tests error boundaries with React 19
2. **Production Build**
- Verifies that the production build completes successfully with React 19
- Checks for the existence of expected output files

@serhalp serhalp added status: needs core review Currently awaiting review from Core team member topic: core Relates to Gatsby's core (e.g. page loading, reporter, state machine) and removed status: triage needed Issue or pull request that need to be triaged and assigned to a reviewer labels Nov 21, 2025
@serhalp serhalp self-assigned this Nov 21, 2025
I believe this is because of a patch bump of `graphql`?
@serhalp serhalp marked this pull request as ready for review November 21, 2025 23:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status: needs core review Currently awaiting review from Core team member topic: core Relates to Gatsby's core (e.g. page loading, reporter, state machine)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

React 19 support

3 participants