Skip to content

Commit f51e57e

Browse files
authored
Merge pull request #3606 from pmndrs/feat/react-19-2-vendor-reconciler
feat: support React 19.2
2 parents 2541e81 + 9f25968 commit f51e57e

File tree

16 files changed

+768
-48
lines changed

16 files changed

+768
-48
lines changed

.github/workflows/test.yml

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,16 @@ on:
66
pull_request: {}
77
jobs:
88
build:
9-
name: Build, lint, and test
9+
name: Build, lint, and test (React ${{ matrix.react-version }})
1010
runs-on: ubuntu-latest
1111

12+
strategy:
13+
fail-fast: false
14+
matrix:
15+
react-version:
16+
- 19.0.0
17+
- latest
18+
1219
steps:
1320
- name: Checkout repo
1421
uses: actions/checkout@v4
@@ -18,11 +25,26 @@ jobs:
1825
with:
1926
node-version: 20
2027

28+
- name: Cache node_modules and Yarn cache
29+
uses: actions/cache@v4
30+
with:
31+
path: |
32+
**/node_modules
33+
.yarn/cache
34+
key: >
35+
${{ runner.os }}-node20-react-${{ matrix.react-version }}-${{ hashFiles('**/yarn.lock') }}
36+
restore-keys: |
37+
${{ runner.os }}-node20-react-${{ matrix.react-version }}-
38+
2139
- name: Install deps and build (with cache)
2240
uses: bahmutov/npm-install@v1
2341
with:
2442
install-command: yarn --immutable --silent
2543

44+
- name: Override React version (${{ matrix.react-version }})
45+
run: |
46+
yarn add @types/react@${{ matrix.react-version }} react@${{ matrix.react-version }} @types/react-dom@${{ matrix.react-version }} react-dom@${{ matrix.react-version }} --dev -W
47+
2648
- name: Check types
2749
run: yarn run typecheck
2850

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ coverage/
33
dist/
44
build/
55
types/
6+
packages/fiber/react-reconciler/
67
# commit types in src
78
!packages/*/src/types/
89
Thumbs.db

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
dist/
22
coverage/
33
node_modules/
4+
packages/fiber/react-reconciler/
45
.yarn/
56
*.gltf
67
*.mdx

example/package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
"dependencies": {
1111
"@react-three/drei": "^9.105.5",
1212
"@use-gesture/react": "latest",
13-
"react": "19.0.0",
14-
"react-dom": "19.0.0",
13+
"react": "19.2.0",
14+
"react-dom": "19.2.0",
1515
"react-use-refs": "^1.0.1",
1616
"three": "^0.172.0",
1717
"three-stdlib": "^2.35.16",
@@ -21,8 +21,8 @@
2121
},
2222
"devDependencies": {
2323
"@types/three": "^0.172.0",
24-
"@types/react": "^19.0.1",
25-
"@types/react-dom": "^19.0.1",
24+
"@types/react": "^19.2.7",
25+
"@types/react-dom": "^19.2.3",
2626
"@vitejs/plugin-react-refresh": "^1.3.6",
2727
"@vitejs/plugin-react": "^4.2.1",
2828
"typescript": "^5.3.3",

example/src/demos/Activity.tsx

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { useRef, useEffectEvent, Suspense, use, useState, Activity } from 'react'
2+
import { useFrame, Canvas } from '@react-three/fiber'
3+
import { Mesh, Group } from 'three'
4+
import { DRACOLoader, GLTFLoader } from 'three-stdlib'
5+
import { Environment } from '@react-three/drei'
6+
7+
const colors = ['orange', 'hotpink', 'cyan', 'lime', 'yellow', 'red', 'blue', 'purple', 'green', 'coral']
8+
9+
function SceneA({ onSelect }: { onSelect: Function }) {
10+
const ref = useRef<Mesh>(null)
11+
const [scale, setScale] = useState(1)
12+
const [color, setColor] = useState(colors[0])
13+
14+
// Stable event handler using the new React 19.2 API
15+
const handleSelect = useEffectEvent(() => onSelect())
16+
17+
useFrame((_, dt) => {
18+
if (ref.current) ref.current.rotation.y += dt * 1.2
19+
})
20+
21+
return (
22+
<mesh
23+
ref={ref}
24+
scale={scale}
25+
position={[-1.5, 0, 0]}
26+
onClick={handleSelect}
27+
onPointerOver={() => {
28+
setScale(1.2)
29+
setColor(colors[Math.floor(Math.random() * colors.length)])
30+
}}
31+
onPointerOut={() => setScale(1)}>
32+
<boxGeometry />
33+
<meshStandardMaterial color={color} />
34+
</mesh>
35+
)
36+
}
37+
38+
const dracoLoader = new DRACOLoader()
39+
dracoLoader.setDecoderPath('https://www.gstatic.com/draco/versioned/decoders/1.5.5/')
40+
41+
const gltfLoader = new GLTFLoader()
42+
gltfLoader.setDRACOLoader(dracoLoader)
43+
44+
const modelPromise = gltfLoader.loadAsync('/apple.gltf')
45+
46+
function SceneB({ onSelect }: { onSelect: Function }) {
47+
const gltf = use(modelPromise)
48+
const ref = useRef<Group>(null)
49+
50+
const [scale, setScale] = useState(5)
51+
const handleSelect = useEffectEvent(() => onSelect())
52+
53+
useFrame((_, dt) => {
54+
if (ref.current) ref.current.rotation.y -= dt * 0.8
55+
})
56+
57+
return (
58+
<primitive
59+
object={gltf.scene}
60+
ref={ref}
61+
scale={scale}
62+
position={[1.5, 0, 0]}
63+
onClick={handleSelect}
64+
onPointerOver={() => setScale(6)}
65+
onPointerOut={() => setScale(5)}
66+
/>
67+
)
68+
}
69+
70+
export default function App() {
71+
const [active, setActive] = useState('A')
72+
73+
return (
74+
<Canvas camera={{ position: [4, 3, 6], fov: 50 }}>
75+
<ambientLight intensity={Math.PI} />
76+
77+
<Environment preset="apartment" />
78+
79+
<Activity mode={active === 'A' ? 'visible' : 'hidden'}>
80+
<Suspense fallback={null}>
81+
<SceneA onSelect={() => setActive('B')} />
82+
</Suspense>
83+
</Activity>
84+
85+
<Activity mode={active === 'B' ? 'visible' : 'hidden'}>
86+
<Suspense fallback={null}>
87+
<SceneB onSelect={() => setActive('A')} />
88+
</Suspense>
89+
</Activity>
90+
</Canvas>
91+
)
92+
}

example/src/demos/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { lazy } from 'react'
22

3+
const Activity = { Component: lazy(() => import('./Activity')) }
34
const AutoDispose = { Component: lazy(() => import('./AutoDispose')) }
45
const ClickAndHover = { Component: lazy(() => import('./ClickAndHover')) }
56
const ContextMenuOverride = { Component: lazy(() => import('./ContextMenuOverride')) }
@@ -28,6 +29,7 @@ const WebGPU = { Component: lazy(() => import('./WebGPU')) }
2829
const FlushSync = { Component: lazy(() => import('./FlushSync')) }
2930

3031
export {
32+
Activity,
3133
AutoDispose,
3234
ClickAndHover,
3335
ContextMenuOverride,

jest.config.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
1+
/** @type {import('jest').Config} */
12
module.exports = {
23
preset: 'ts-jest',
34
testEnvironment: 'jsdom',
45
testPathIgnorePatterns: ['/node_modules/'],
6+
globals: {
7+
'ts-jest': {
8+
tsconfig: {
9+
allowJs: true,
10+
},
11+
},
12+
},
13+
transform: {
14+
'^.+\\.jsx?$': ['ts-jest', { useESM: true }],
15+
},
516
moduleNameMapper: {
617
'^three$': '<rootDir>/node_modules/three/build/three.cjs',
718
},

package.json

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
},
2525
"scripts": {
2626
"changeset:add": "changeset add",
27-
"postinstall": "preconstruct dev",
27+
"postinstall": "preconstruct dev && yarn patch-react-reconciler",
28+
"patch-react-reconciler": "vite build",
2829
"build": "preconstruct build",
2930
"examples": "yarn workspace example dev",
3031
"dev": "preconstruct dev",
@@ -53,8 +54,8 @@
5354
"@preconstruct/cli": "^2.1.5",
5455
"@testing-library/react": "^15.0.2",
5556
"@types/jest": "^29.2.5",
56-
"@types/react": "^19.0.1",
57-
"@types/react-dom": "^19.0.1",
57+
"@types/react": "^19.2.7",
58+
"@types/react-dom": "^19.2.3",
5859
"@types/react-native": "0.69.5",
5960
"@types/scheduler": "0.23.0",
6061
"@types/three": "^0.172.0",
@@ -77,14 +78,15 @@
7778
"lint-staged": "^12.3.7",
7879
"prettier": "^2.6.1",
7980
"pretty-quick": "^3.1.3",
80-
"react": "^19.0.0",
81-
"react-dom": "^19.0.0",
81+
"react": "^19.2.0",
82+
"react-dom": "^19.2.0",
8283
"react-native": "0.69.3",
8384
"react-nil": "^2.0.0",
8485
"three": "^0.172.0",
8586
"three-stdlib": "^2.35.16",
8687
"ts-jest": "^29.1.2",
87-
"typescript": "^4.6.3"
88+
"typescript": "^4.6.3",
89+
"vite": "^6.4.1"
8890
},
8991
"packageManager": "[email protected]+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
9092
}

packages/fiber/package.json

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,18 @@
4242
"scripts": {
4343
"prebuild": "cp ../../readme.md readme.md"
4444
},
45+
"devDependencies": {
46+
"@types/react-reconciler": "^0.32.3",
47+
"react-reconciler": "^0.33.0"
48+
},
4549
"dependencies": {
4650
"@babel/runtime": "^7.17.8",
47-
"@types/react-reconciler": "^0.32.0",
4851
"@types/webxr": "*",
4952
"base64-js": "^1.5.1",
5053
"buffer": "^6.0.3",
5154
"its-fine": "^2.0.0",
52-
"react-reconciler": "^0.31.0",
5355
"react-use-measure": "^2.1.7",
54-
"scheduler": "^0.25.0",
56+
"scheduler": "^0.27.0",
5557
"suspend-react": "^0.1.3",
5658
"use-sync-external-store": "^1.4.0",
5759
"zustand": "^5.0.3"
@@ -61,8 +63,8 @@
6163
"expo-asset": ">=8.4",
6264
"expo-file-system": ">=11.0",
6365
"expo-gl": ">=11.0",
64-
"react": "^19.0.0",
65-
"react-dom": "^19.0.0",
66+
"react": ">=19 <19.3",
67+
"react-dom": ">=19 <19.3",
6668
"react-native": ">=0.78",
6769
"three": ">=0.156"
6870
},

packages/fiber/src/core/reconciler.tsx

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import packageData from '../../package.json'
22
import * as THREE from 'three'
33
import * as React from 'react'
4-
import Reconciler from 'react-reconciler'
5-
import { ContinuousEventPriority, DiscreteEventPriority, DefaultEventPriority } from 'react-reconciler/constants'
4+
import Reconciler from '../../react-reconciler/index.js'
5+
import {
6+
ContinuousEventPriority,
7+
DiscreteEventPriority,
8+
DefaultEventPriority,
9+
} from '../../react-reconciler/constants.js'
610
import { unstable_IdlePriority as idlePriority, unstable_scheduleCallback as scheduleCallback } from 'scheduler'
711
import {
812
diffProps,
@@ -561,7 +565,6 @@ export const reconciler = /* @__PURE__ */ createReconciler<
561565
requestPostPaintCallback() {},
562566
maySuspendCommit: () => false,
563567
preloadInstance: () => true, // true indicates already loaded
564-
startSuspendingCommit() {},
565568
suspendInstance() {},
566569
waitForCommitToBeReady: () => null,
567570
NotPendingTransition: null,
@@ -602,4 +605,69 @@ export const reconciler = /* @__PURE__ */ createReconciler<
602605
// @ts-ignore DefinitelyTyped is not up to date
603606
rendererPackageName: '@react-three/fiber',
604607
rendererVersion: packageData.version,
608+
609+
// https://github.com/facebook/react/pull/31975
610+
// https://github.com/facebook/react/pull/31999
611+
applyViewTransitionName(_instance: any, _name: any, _className: any) {},
612+
restoreViewTransitionName(_instance: any, _props: any) {},
613+
cancelViewTransitionName(_instance: any, _name: any, _props: any) {},
614+
cancelRootViewTransitionName(_rootContainer: any) {},
615+
restoreRootViewTransitionName(_rootContainer: any) {},
616+
InstanceMeasurement: null,
617+
measureInstance: (_instance: any) => null,
618+
wasInstanceInViewport: (_measurement: any): boolean => true,
619+
hasInstanceChanged: (_oldMeasurement: any, _newMeasurement: any): boolean => false,
620+
hasInstanceAffectedParent: (_oldMeasurement: any, _newMeasurement: any): boolean => false,
621+
622+
// https://github.com/facebook/react/pull/32002
623+
// https://github.com/facebook/react/pull/34486
624+
suspendOnActiveViewTransition(_state: any, _container: any) {},
625+
626+
// https://github.com/facebook/react/pull/32451
627+
// https://github.com/facebook/react/pull/32760
628+
startGestureTransition: () => null,
629+
startViewTransition: () => null,
630+
stopViewTransition(_transition: null) {},
631+
632+
// https://github.com/facebook/react/pull/32038
633+
createViewTransitionInstance: (_name: string): null => null,
634+
635+
// https://github.com/facebook/react/pull/32379
636+
// https://github.com/facebook/react/pull/32786
637+
getCurrentGestureOffset(_provider: null): number {
638+
throw new Error('startGestureTransition is not yet supported in react-three-fiber.')
639+
},
640+
641+
// https://github.com/facebook/react/pull/32500
642+
cloneMutableInstance(instance: any, _keepChildren: any) {
643+
return instance
644+
},
645+
cloneMutableTextInstance(textInstance: any) {
646+
return textInstance
647+
},
648+
cloneRootViewTransitionContainer(_rootContainer: any) {
649+
throw new Error('Not implemented.')
650+
},
651+
removeRootViewTransitionClone(_rootContainer: any, _clone: any) {
652+
throw new Error('Not implemented.')
653+
},
654+
655+
// https://github.com/facebook/react/pull/32465
656+
createFragmentInstance: (_fiber: any): null => null,
657+
updateFragmentInstanceFiber(_fiber: any, _instance: any): void {},
658+
commitNewChildToFragmentInstance(_child: any, _fragmentInstance: any): void {},
659+
deleteChildFromFragmentInstance(_child: any, _fragmentInstance: any): void {},
660+
661+
// https://github.com/facebook/react/pull/32653
662+
measureClonedInstance: (_instance: any) => null,
663+
664+
// https://github.com/facebook/react/pull/32819
665+
maySuspendCommitOnUpdate: (_type: any, _oldProps: any, _newProps: any) => false,
666+
maySuspendCommitInSyncRender: (_type: any, _props: any) => false,
667+
668+
// https://github.com/facebook/react/pull/34486
669+
startSuspendingCommit: () => null,
670+
671+
// https://github.com/facebook/react/pull/34522
672+
getSuspendedCommitReason: (_state: any, _rootContainer: any) => null,
605673
})

0 commit comments

Comments
 (0)