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. You can safely upgrade to this release while staying on React 18.

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

Note

Community plugins may not have been updated yet to support React 19, so please check their repository for the current status. All plugins managed by the Gatsby team (in the gatsbyjs/gatsby repository) have been updated.

To upgrade to React 19, first upgrade gatsby and all your dependencies that start with gatsby- to the latest version. (Check out this guide if you need help with that.)

Tip

If you use npm 7 or higher you’ll want to use the --legacy-peer-deps option. For example, if you use gatsby and gatsby-plugin-postcss:

npm install --legacy-peer-deps gatsby@latest gatsby-plugin-postcss@latest

Then, follow the React 19 upgrade guide. No other changes are required.

Please note:

  • Some new React 19 features may not be available yet via Gatsby.
  • Gatsby Partial Hydration, an experimental feature for three years now, is known to be incompatible with React 19 at this time. If you rely on this feature, do not upgrade to React 19.

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 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
@serhalp serhalp requested a review from pieh November 24, 2025 17:10
@serhalp serhalp merged commit 9e39c05 into master Nov 27, 2025
74 checks passed
@serhalp serhalp deleted the feat/support-react-19 branch November 27, 2025 13:40
serhalp added a commit that referenced this pull request Dec 5, 2025
In #39306 we introduced a workaround that wraps the Gatsby
Head API elements in an `<svg>` wrapper. React under the hood creates these children elements in the
"SVG" namespace. In the case of `<title>`, this is problematic because when we insert it into the
real `<head>`, browsers (as far as we can tell) do not trigger an update of `document.title` because
this only occurs for `<title>` nodes in the HTML namespace. A node's namespace is immutable, even
when using `cloneNode()` or `importNode()`. The only way to "reset" the namespace is to recreate a
node.
serhalp added a commit that referenced this pull request Dec 5, 2025
In #39306 we introduced a workaround that wraps the Gatsby
Head API elements in an `<svg>` wrapper. React under the hood creates these children elements in the
"SVG" namespace. In the case of `<title>`, this is problematic because when we insert it into the
real `<head>`, browsers (as far as we can tell) do not trigger an update of `document.title` because
this only occurs for `<title>` nodes in the HTML namespace. A node's namespace is immutable, even
when using `cloneNode()` or `importNode()`. The only way to "reset" the namespace is to recreate a
node.
serhalp added a commit that referenced this pull request Dec 5, 2025
In #39306 we introduced a workaround that wraps the Gatsby
Head API elements in an `<svg>` wrapper. React under the hood creates these children elements in the
"SVG" namespace. In the case of `<title>`, this is problematic because when we insert it into the
real `<head>`, browsers (as far as we can tell) do not trigger an update of `document.title` because
this only occurs for `<title>` nodes in the HTML namespace. A node's namespace is immutable, even
when using `cloneNode()` or `importNode()`. The only way to "reset" the namespace is to recreate a
node.
serhalp added a commit that referenced this pull request Dec 5, 2025
In #39306 we introduced a workaround that wraps the Gatsby
Head API elements in an `<svg>` wrapper. React under the hood creates these children elements in the
"SVG" namespace. In the case of `<title>`, this is problematic because when we insert it into the
real `<head>`, browsers (as far as we can tell) do not trigger an update of `document.title` because
this only occurs for `<title>` nodes in the HTML namespace. A node's namespace is immutable, even
when using `cloneNode()` or `importNode()`. The only way to "reset" the namespace is to recreate a
node.
serhalp added a commit that referenced this pull request Dec 5, 2025
In #39306 we introduced a workaround that wraps the Gatsby
Head API elements in an `<svg>` wrapper. React under the hood creates these children elements in the
"SVG" namespace. In the case of `<title>`, this is problematic because when we insert it into the
real `<head>`, browsers (as far as we can tell) do not trigger an update of `document.title` because
this only occurs for `<title>` nodes in the HTML namespace. A node's namespace is immutable, even
when using `cloneNode()` or `importNode()`. The only way to "reset" the namespace is to recreate a
node.
pieh added a commit that referenced this pull request Dec 22, 2025
…39382)

* chore: add retries to flaky test

* refactor: move guard earlier and colocate with comment

* ci: disable browserslist console warnings

These are nondeterministic and conflict with test output assertions

* fix: ensure inserting <title> in <head> updates `document.title`

In #39306 we introduced a workaround that wraps the Gatsby
Head API elements in an `<svg>` wrapper. React under the hood creates these children elements in the
"SVG" namespace. In the case of `<title>`, this is problematic because when we insert it into the
real `<head>`, browsers (as far as we can tell) do not trigger an update of `document.title` because
this only occurs for `<title>` nodes in the HTML namespace. A node's namespace is immutable, even
when using `cloneNode()` or `importNode()`. The only way to "reset" the namespace is to recreate a
node.

* fix: remove svg workaround, use itemProp instead (#39385)

* test: expand navigation tests to test if effects are applied and not just if tags are inserted/updated/removed

* fix: remove svg workaround, use itemProp instead

* fix: don't break rules of hooks in createElement patch

---------

Co-authored-by: Michal Piechowiak <[email protected]>
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 — now in public preview!

3 participants