Skip to content

Commit 24546b0

Browse files
committed
feat: support React 19
1 parent a216ded commit 24546b0

File tree

10 files changed

+257
-32
lines changed

10 files changed

+257
-32
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# React 19 Compatibility Test
2+
3+
This directory contains end-to-end tests to verify Gatsby's compatibility with React 19.
4+
5+
## Running the Tests
6+
7+
1. Install dependencies:
8+
9+
```bash
10+
npm install
11+
```
12+
13+
2. Run the tests:
14+
```bash
15+
npm test
16+
```
17+
18+
## Test Cases
19+
20+
1. **Development Server**
21+
22+
- Verifies that the development server starts with React 19
23+
- Tests React 19 state updates and hooks
24+
- Tests error boundaries with React 19
25+
26+
2. **Production Build**
27+
- Verifies that the production build completes successfully with React 19
28+
- Checks for the existence of expected output files
29+
30+
## Dependencies
31+
32+
- React 19.0.0 or later
33+
- React DOM 19.0.0 or later
34+
- Gatsby (linked to local development version)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { defineConfig } from "cypress"
2+
3+
export default defineConfig({
4+
e2e: {
5+
baseUrl: "http://localhost:8000",
6+
supportFile: false,
7+
},
8+
component: {
9+
devServer: {
10+
framework: "create-react-app",
11+
bundler: "webpack",
12+
},
13+
},
14+
})
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/// <reference types="cypress" />
2+
3+
describe("React 19 Compatibility", () => {
4+
beforeEach(() => {
5+
cy.visit("/")
6+
})
7+
8+
it("renders the home page", () => {
9+
cy.get("h1").should("contain", "Gatsby + React 19 Test")
10+
})
11+
12+
it("handles React 19 state updates", () => {
13+
// Initial state
14+
cy.get("p").should("contain", "Count: 0")
15+
16+
// Test state update
17+
cy.get('[data-testid="increment"]').click()
18+
cy.get("p").should("contain", "Count: 1")
19+
})
20+
21+
it("handles React 19 error boundaries", () => {
22+
// Test error boundary
23+
cy.get('[data-testid="error-button"]').click()
24+
cy.get("div").should("contain", "Error: Test error")
25+
})
26+
})
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module.exports = {
2+
siteMetadata: {
3+
title: `Gatsby React 19 Test`,
4+
},
5+
plugins: [],
6+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"name": "gatsby-react-19-compatibility",
3+
"private": true,
4+
"description": "Test React 19 compatibility",
5+
"version": "1.0.0",
6+
"author": "Gatsby Team",
7+
"dependencies": {
8+
"gatsby": "*",
9+
"react": "^19.0.0",
10+
"react-dom": "^19.0.0"
11+
},
12+
"devDependencies": {
13+
"@testing-library/cypress": "^9.0.0",
14+
"cypress": "^12.0.0",
15+
"typescript": "^4.9.5"
16+
},
17+
"keywords": [
18+
"gatsby"
19+
],
20+
"license": "MIT",
21+
"scripts": {
22+
"develop": "gatsby develop",
23+
"build": "gatsby build",
24+
"clean": "gatsby clean",
25+
"serve": "gatsby serve",
26+
"test": "start-server-and-test develop http://localhost:8000 'cypress run'",
27+
"test:watch": "start-server-and-test develop http://localhost:8000 'cypress open'"
28+
},
29+
"engines": {
30+
"node": ">=18.0.0"
31+
}
32+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import React from "react"
2+
3+
export default function Home() {
4+
// Test React 19 features
5+
const [count, setCount] = React.useState(0)
6+
7+
// Test error boundaries
8+
const [error, setError] = React.useState(null)
9+
10+
if (error) {
11+
// Test error boundaries
12+
return <div>Error: {error.message}</div>
13+
}
14+
15+
return (
16+
<div>
17+
<h1>Gatsby + React 19 Test</h1>
18+
<p>Count: {count}</p>
19+
<button onClick={() => setCount(c => c + 1)} data-testid="increment">
20+
Increment
21+
</button>
22+
<button
23+
onClick={() => setError(new Error("Test error"))}
24+
data-testid="error-button"
25+
>
26+
Trigger Error
27+
</button>
28+
</div>
29+
)
30+
}

packages/gatsby/cache-dir/production-app.js

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,19 @@ navigationInit()
4646
const reloadStorageKey = `gatsby-reload-compilation-hash-match`
4747

4848
apiRunnerAsync(`onClientEntry`).then(() => {
49+
// Set up React 19 error handling
50+
const handleUncaughtError = (error, errorInfo) => {
51+
// Report uncaught errors to any error tracking service
52+
console.error(`Uncaught error:`, error, errorInfo)
53+
apiRunner(`onUncaughtError`, { error, errorInfo })
54+
}
55+
56+
const handleCaughtError = (error, errorInfo) => {
57+
// Report caught errors to any error tracking service
58+
console.error(`Caught error:`, error, errorInfo)
59+
apiRunner(`onCaughtError`, { error, errorInfo })
60+
}
61+
4962
// Let plugins register a service worker. The plugin just needs
5063
// to return true.
5164
if (apiRunner(`registerServiceWorker`).filter(Boolean).length > 0) {
@@ -277,16 +290,23 @@ apiRunnerAsync(`onClientEntry`).then(() => {
277290

278291
// Client only pages have any empty body so we just do a normal
279292
// render to avoid React complaining about hydration mis-matches.
280-
let defaultRenderer = render
293+
let defaultRenderer = (Component, el) =>
294+
render(Component, el, {
295+
onUncaughtError: handleUncaughtError,
296+
onCaughtError: handleCaughtError,
297+
})
298+
281299
if (focusEl && focusEl.children.length) {
282-
defaultRenderer = hydrate
300+
defaultRenderer = (Component, el) =>
301+
hydrate(Component, el, {
302+
onUncaughtError: handleUncaughtError,
303+
onCaughtError: handleCaughtError,
304+
})
283305
}
284306

285-
const renderer = apiRunner(
286-
`replaceHydrateFunction`,
287-
undefined,
307+
const renderer =
308+
apiRunner(`replaceHydrateFunction`, undefined, defaultRenderer)[0] ||
288309
defaultRenderer
289-
)[0]
290310

291311
function runRender() {
292312
const rootElement =

packages/gatsby/cache-dir/react-dom-utils.js

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,29 @@ const map = new WeakMap()
33
export function reactDOMUtils() {
44
const reactDomClient = require(`react-dom/client`)
55

6-
const render = (Component, el) => {
6+
const render = (Component, el, options = {}) => {
77
let root = map.get(el)
88
if (!root) {
9-
map.set(el, (root = reactDomClient.createRoot(el)))
9+
const rootOptions = {}
10+
11+
// Add React 19 error handling options if available
12+
if (options.onUncaughtError || options.onCaughtError) {
13+
rootOptions.onUncaughtError = options.onUncaughtError
14+
rootOptions.onCaughtError = options.onCaughtError
15+
}
16+
17+
map.set(el, (root = reactDomClient.createRoot(el, rootOptions)))
1018
}
1119
root.render(Component)
1220
}
1321

14-
const hydrate = (Component, el) => reactDomClient.hydrateRoot(el, Component)
22+
const hydrate = (Component, el, options = {}) => {
23+
const root = reactDomClient.hydrateRoot(el, Component, {
24+
onUncaughtError: options.onUncaughtError,
25+
onCaughtError: options.onCaughtError,
26+
})
27+
return root
28+
}
1529

1630
return { render, hydrate }
1731
}

packages/gatsby/package.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
"@nodelib/fs.walk": "^1.2.8",
3333
"@parcel/cache": "2.8.3",
3434
"@parcel/core": "2.8.3",
35-
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.10",
35+
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.0",
3636
"@sigmacomputing/babel-plugin-lodash": "^3.3.5",
3737
"@types/http-proxy": "^1.17.11",
3838
"@typescript-eslint/eslint-plugin": "^5.60.1",
@@ -147,8 +147,8 @@
147147
"query-string": "^6.14.1",
148148
"raw-loader": "^4.0.2",
149149
"react-dev-utils": "^12.0.1",
150-
"react-refresh": "^0.14.0",
151-
"react-server-dom-webpack": "0.0.0-experimental-c8b778b7f-20220825",
150+
"react-refresh": "^0.14.1",
151+
"react-server-dom-webpack": "^0.0.0-experimental-6f3f5f5b-20240603",
152152
"redux": "4.2.1",
153153
"redux-thunk": "^2.4.2",
154154
"resolve-from": "^5.0.0",
@@ -191,7 +191,7 @@
191191
"@types/micromatch": "^4.0.2",
192192
"@types/normalize-path": "^3.0.0",
193193
"@types/reach__router": "^1.3.11",
194-
"@types/react-dom": "^18.2.6",
194+
"@types/react-dom": "^19.0.0",
195195
"@types/semver": "^7.5.0",
196196
"@types/signal-exit": "^3.0.1",
197197
"@types/string-similarity": "^4.0.0",
@@ -201,8 +201,8 @@
201201
"copyfiles": "^2.4.1",
202202
"cross-env": "^7.0.3",
203203
"documentation": "^13.2.5",
204-
"react": "^18.2.0",
205-
"react-dom": "^18.2.0",
204+
"react": "^19.0.0",
205+
"react-dom": "^19.0.0",
206206
"rimraf": "^3.0.2",
207207
"typescript": "^5.1.6",
208208
"xhr-mock": "^2.5.1",
@@ -250,8 +250,8 @@
250250
"main": "cache-dir/commonjs/gatsby-browser-entry.js",
251251
"module": "cache-dir/gatsby-browser-entry.js",
252252
"peerDependencies": {
253-
"react": "^18.0.0 || ^0.0.0",
254-
"react-dom": "^18.0.0 || ^0.0.0"
253+
"react": "^18.0.0 || ^19.0.0 || ^0.0.0",
254+
"react-dom": "^18.0.0 || ^19.0.0 || ^0.0.0"
255255
},
256256
"repository": {
257257
"type": "git",

0 commit comments

Comments
 (0)