From 61f72677737c0abbebe7fb6df0ad20ce3288d1b7 Mon Sep 17 00:00:00 2001 From: Ben Cook Date: Wed, 21 Oct 2026 11:15:24 +1100 Subject: [PATCH 1/2] chore: update React dev dependencies to 19.3 React 19 types no longer declare `ReactDOM.render` or `unmountComponentAtNode`, so type the React 16/17 code path locally. Node 24 ships a read-only `Symbol.dispose`, so only polyfill it when missing. --- package.json | 12 ++--- src/renderWithoutAct.tsx | 14 +++-- tests/polyfill.js | 2 +- yarn.lock | 111 +++++++++++++++++++-------------------- 4 files changed, 72 insertions(+), 67 deletions(-) diff --git a/package.json b/package.json index b1e93df2f..df72a8080 100644 --- a/package.json +++ b/package.json @@ -64,8 +64,8 @@ "other" ], "dependencies": { - "@testing-library/dom": "^10.4.0", - "@testing-library/react": "^16.0.1", + "@testing-library/dom": "^10.4.2", + "@testing-library/react": "^16.3.3", "jsdom": "^25.0.1", "rehackt": "^0.1.0" }, @@ -75,16 +75,16 @@ "@testing-library/user-event": "^14.5.2", "@tsconfig/recommended": "^1.0.7", "@types/jsdom": "^21.1.7", - "@types/react": "^18", - "@types/react-dom": "^18", + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", "concurrently": "^9.0.1", "expect": "^29.7.0", "kcd-scripts": "^16.0.0", "pkg-pr-new": "^0.0.29", "prettier": "^3.3.3", "publint": "^0.2.11", - "react": "19.0.0", - "react-dom": "19.0.0", + "react": "19.3.1", + "react-dom": "19.3.1", "react-error-boundary": "^4.0.13", "ts-jest-resolver": "^2.0.1", "tsup": "^8.3.0", diff --git a/src/renderWithoutAct.tsx b/src/renderWithoutAct.tsx index 26e9d3cb5..c471e5810 100644 --- a/src/renderWithoutAct.tsx +++ b/src/renderWithoutAct.tsx @@ -18,9 +18,9 @@ import { // Ideally we'd just use a WeakMap where containers are keys and roots are values. // We use two variables so that we can bail out in constant time when we render with a new container (most common use case) -const mountedContainers: Set = new Set() +const mountedContainers: Set = new Set() const mountedRootEntries: Array<{ - container: import('react-dom').Container + container: ReactDOMClient.Container root: ReturnType }> = [] @@ -187,13 +187,19 @@ async function _renderWithoutAct( }) } +// `render` and `unmountComponentAtNode` only exist at runtime in React 16/17 and were removed from the React 19 types. +const LegacyReactDOM = ReactDOM as unknown as { + render(element: React.ReactElement, container: ReactDOMClient.Container): void + unmountComponentAtNode(container: ReactDOMClient.Container): boolean +} + function createLegacyRoot(container: ReactDOMClient.Container) { return { render(element: React.ReactNode) { - ReactDOM.render(element as unknown as React.ReactElement, container) + LegacyReactDOM.render(element as unknown as React.ReactElement, container) }, unmount() { - ReactDOM.unmountComponentAtNode(container) + LegacyReactDOM.unmountComponentAtNode(container) }, } } diff --git a/tests/polyfill.js b/tests/polyfill.js index bcd8e511b..c7614881a 100644 --- a/tests/polyfill.js +++ b/tests/polyfill.js @@ -3,4 +3,4 @@ import {TextEncoder, TextDecoder} from 'util' global.TextEncoder = TextEncoder global.TextDecoder = TextDecoder -Symbol.dispose = Symbol('Symbol.dispose') +if (!Symbol.dispose) Symbol.dispose = Symbol('Symbol.dispose') diff --git a/yarn.lock b/yarn.lock index 4e0e5e050..723e93a37 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2598,19 +2598,19 @@ __metadata: languageName: node linkType: hard -"@testing-library/dom@npm:^10.4.0": - version: 10.4.0 - resolution: "@testing-library/dom@npm:10.4.0" +"@testing-library/dom@npm:^10.4.2": + version: 10.4.2 + resolution: "@testing-library/dom@npm:10.4.2" dependencies: "@babel/code-frame": "npm:^7.10.4" "@babel/runtime": "npm:^7.12.5" "@types/aria-query": "npm:^5.0.1" aria-query: "npm:5.3.0" - chalk: "npm:^4.1.0" dom-accessibility-api: "npm:^0.5.9" lz-string: "npm:^1.5.0" + picocolors: "npm:1.1.1" pretty-format: "npm:^27.0.2" - checksum: 10c0/0352487720ecd433400671e773df0b84b8268fb3fe8e527cdfd7c11b1365b398b4e0eddba6e7e0c85e8d615f48257753283fccec41f6b986fd6c85f15eb5f84f + checksum: 10c0/22c3aded3a54fc4f1a0c54aa75a881bb65e45f0128f3c5c881f9e571e593c6364c036f71b733dd0856ff2ba0debd59106e206f6764ee771dd8ed7cf82ceaa618 languageName: node linkType: hard @@ -2620,13 +2620,13 @@ __metadata: dependencies: "@arethetypeswrong/cli": "npm:^0.16.4" "@jest/globals": "npm:^29.7.0" - "@testing-library/dom": "npm:^10.4.0" - "@testing-library/react": "npm:^16.0.1" + "@testing-library/dom": "npm:^10.4.2" + "@testing-library/react": "npm:^16.3.3" "@testing-library/user-event": "npm:^14.5.2" "@tsconfig/recommended": "npm:^1.0.7" "@types/jsdom": "npm:^21.1.7" - "@types/react": "npm:^18" - "@types/react-dom": "npm:^18" + "@types/react": "npm:^19.3.0" + "@types/react-dom": "npm:^19.3.0" concurrently: "npm:^9.0.1" expect: "npm:^29.7.0" jsdom: "npm:^25.0.1" @@ -2634,8 +2634,8 @@ __metadata: pkg-pr-new: "npm:^0.0.29" prettier: "npm:^3.3.3" publint: "npm:^0.2.11" - react: "npm:19.0.0" - react-dom: "npm:19.0.0" + react: "npm:19.3.1" + react-dom: "npm:19.3.1" react-error-boundary: "npm:^4.0.13" rehackt: "npm:^0.1.0" ts-jest-resolver: "npm:^2.0.1" @@ -2649,23 +2649,23 @@ __metadata: languageName: unknown linkType: soft -"@testing-library/react@npm:^16.0.1": - version: 16.0.1 - resolution: "@testing-library/react@npm:16.0.1" +"@testing-library/react@npm:^16.3.3": + version: 16.3.3 + resolution: "@testing-library/react@npm:16.3.3" dependencies: "@babel/runtime": "npm:^7.12.5" peerDependencies: "@testing-library/dom": ^10.0.0 - "@types/react": ^18.0.0 - "@types/react-dom": ^18.0.0 - react: ^18.0.0 - react-dom: ^18.0.0 + "@types/react": ^18.0.0 || ^19.0.0 + "@types/react-dom": ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 peerDependenciesMeta: "@types/react": optional: true "@types/react-dom": optional: true - checksum: 10c0/67d05dec5ad5a2e6f92b6a3234af785435c7bb62bdbf12f3bfc89c9bca0c871a189e88c4ba023ed4cea504704c87c6ac7e86e24a3962df6c521ae89b62f48ff7 + checksum: 10c0/87d65cca71a39c93fffa23746a751a7037a4b4e939ea6b234af5188e4bdec96a7568b05000db109b5b76c686899e93a329a594d7e690674e30b35594c87c9e1d languageName: node linkType: hard @@ -2883,29 +2883,21 @@ __metadata: languageName: node linkType: hard -"@types/prop-types@npm:*": - version: 15.7.13 - resolution: "@types/prop-types@npm:15.7.13" - checksum: 10c0/1b20fc67281902c6743379960247bc161f3f0406ffc0df8e7058745a85ea1538612109db0406290512947f9632fe9e10e7337bf0ce6338a91d6c948df16a7c61 - languageName: node - linkType: hard - -"@types/react-dom@npm:^18": - version: 18.3.0 - resolution: "@types/react-dom@npm:18.3.0" - dependencies: - "@types/react": "npm:*" - checksum: 10c0/6c90d2ed72c5a0e440d2c75d99287e4b5df3e7b011838cdc03ae5cd518ab52164d86990e73246b9d812eaf02ec351d74e3b4f5bd325bf341e13bf980392fd53b +"@types/react-dom@npm:^19.3.0": + version: 19.3.0 + resolution: "@types/react-dom@npm:19.3.0" + peerDependencies: + "@types/react": ^19.3.0 + checksum: 10c0/ce0066d28b62bc6afdd17ca84756b8fbe0f6ffc0c8b1e60739abc30e81510da2edbc1007e6ab3d974d30f5a8c25bb0f7de8d7ce09736c909fe84c0f9aed0b946 languageName: node linkType: hard -"@types/react@npm:*, @types/react@npm:^18": - version: 18.3.11 - resolution: "@types/react@npm:18.3.11" +"@types/react@npm:^19.3.0": + version: 19.3.0 + resolution: "@types/react@npm:19.3.0" dependencies: - "@types/prop-types": "npm:*" - csstype: "npm:^3.0.2" - checksum: 10c0/ce80512246ca5bda69db85b9f4f1835189334acfb6b2c4f3eda8cabff1ff1a3ea9ce4f3b895bdbc18c94140aa45592331aa3fdeb557f525c1b048de7ce84fc0e + csstype: "npm:^3.2.2" + checksum: 10c0/fbcabf303f935ca9f36ed2dcf81751cf84744098c5a683c8a62a0fa7edd31714b48d5c46b12fd0014f7f2dfe4d9ec6de428a850748eec719a70294e51029551b languageName: node linkType: hard @@ -4421,10 +4413,10 @@ __metadata: languageName: node linkType: hard -"csstype@npm:^3.0.2": - version: 3.1.3 - resolution: "csstype@npm:3.1.3" - checksum: 10c0/80c089d6f7e0c5b2bd83cf0539ab41474198579584fa10d86d0cafe0642202343cbc119e076a0b1aece191989477081415d66c9fefbf3c957fc2fc4b7009f248 +"csstype@npm:^3.2.2": + version: 3.2.3 + resolution: "csstype@npm:3.2.3" + checksum: 10c0/cd29c51e70fa822f1cecd8641a1445bed7063697469d35633b516e60fe8c1bde04b08f6c5b6022136bb669b64c63d4173af54864510fbb4ee23281801841a3ce languageName: node linkType: hard @@ -8865,6 +8857,13 @@ __metadata: languageName: node linkType: hard +"picocolors@npm:1.1.1": + version: 1.1.1 + resolution: "picocolors@npm:1.1.1" + checksum: 10c0/e2e3e8170ab9d7c7421969adaa7e1b31434f789afb9b3f115f6b96d91945041ac3ceb02e9ec6fe6510ff036bcc0bf91e69a1772edc0b707e12b19c0f2d6bcf58 + languageName: node + linkType: hard + "picocolors@npm:^1.0.0, picocolors@npm:^1.0.1, picocolors@npm:^1.1.0": version: 1.1.0 resolution: "picocolors@npm:1.1.0" @@ -9169,14 +9168,14 @@ __metadata: languageName: node linkType: hard -"react-dom@npm:19.0.0": - version: 19.0.0 - resolution: "react-dom@npm:19.0.0" +"react-dom@npm:19.3.1": + version: 19.3.1 + resolution: "react-dom@npm:19.3.1" dependencies: - scheduler: "npm:^0.25.0" + scheduler: "npm:^0.28.0" peerDependencies: - react: ^19.0.0 - checksum: 10c0/a36ce7ab507b237ae2759c984cdaad4af4096d8199fb65b3815c16825e5cfeb7293da790a3fc2184b52bfba7ba3ff31c058c01947aff6fd1a3701632aabaa6a9 + react: ^19.3.1 + checksum: 10c0/382700916313b2ebc46acee80642f300bb8601eac6a9e7fefade5b8b30e66b05bf3aaa56a3c964589fcd578d27b486e847470c2697d879264e766661ff49fac7 languageName: node linkType: hard @@ -9212,10 +9211,10 @@ __metadata: languageName: node linkType: hard -"react@npm:19.0.0": - version: 19.0.0 - resolution: "react@npm:19.0.0" - checksum: 10c0/9cad8f103e8e3a16d15cb18a0d8115d8bd9f9e1ce3420310aea381eb42aa0a4f812cf047bb5441349257a05fba8a291515691e3cb51267279b2d2c3253f38471 +"react@npm:19.3.1": + version: 19.3.1 + resolution: "react@npm:19.3.1" + checksum: 10c0/f0ed036d1223562c09c8347858ca712733ea6e27a30db709ee9c3f3f176d3e9e767afce735d59956addf3d030102e256bba82dea635fc1bdb0e01107e98fef10 languageName: node linkType: hard @@ -9709,10 +9708,10 @@ __metadata: languageName: node linkType: hard -"scheduler@npm:^0.25.0": - version: 0.25.0 - resolution: "scheduler@npm:0.25.0" - checksum: 10c0/a4bb1da406b613ce72c1299db43759526058fdcc413999c3c3e0db8956df7633acf395cb20eb2303b6a65d658d66b6585d344460abaee8080b4aa931f10eaafe +"scheduler@npm:^0.28.0": + version: 0.28.0 + resolution: "scheduler@npm:0.28.0" + checksum: 10c0/783c8e5ce89d41273a02ed6dc0e3d24bd60e05c9e8f7e3adfc6c82e7c69935500d90e343f1ee26d592fc7504e8a2532573c6368def3e93bfe812391fa53efaf4 languageName: node linkType: hard From 482ecae5e99bfebb2fa0d4054a00fed5aa9d495b Mon Sep 17 00:00:00 2001 From: Ben Cook Date: Wed, 21 Oct 2026 11:15:25 +1100 Subject: [PATCH 2/2] fix: wait for effects before `render` and `rerender` resolve Since React 19.1, effects can run in a later task than their commit, so code that subscribes in `useEffect` was not yet subscribed when `await render()` resolved. --- src/renderStream/createRenderStream.tsx | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/renderStream/createRenderStream.tsx b/src/renderStream/createRenderStream.tsx index e6518146d..31006bcd7 100644 --- a/src/renderStream/createRenderStream.tsx +++ b/src/renderStream/createRenderStream.tsx @@ -136,7 +136,8 @@ export function createRenderStream< let nextRender: Promise> | undefined, resolveNextRender: ((render: Render) => void) | undefined, - rejectNextRender: ((error: unknown) => void) | undefined + rejectNextRender: ((error: unknown) => void) | undefined, + resolveEffectsFlushed: (() => void) | undefined function resetNextRender() { nextRender = undefined resolveNextRender = undefined @@ -240,7 +241,20 @@ export function createRenderStream< } let iteratorPosition = 0 + /** + * Since React 19.1, effects can run in a later task than their commit. + * `Wrapper` only rerenders on `render` and `rerender`, and parent effects run + * after child effects, so this resolves once all effects of that commit ran. + */ + function nextEffectsFlushed() { + return new Promise(resolve => { + resolveEffectsFlushed = resolve + }) + } function Wrapper({children}: {children: React.ReactNode}) { + React.useEffect(() => { + resolveEffectsFlushed?.() + }) return ( @@ -254,6 +268,7 @@ export function createRenderStream< ui: React.ReactNode, options?: RenderOptions, ) => { + const effectsFlushed = nextEffectsFlushed() const ret = await renderWithoutAct(ui, { ...options, wrapper: props => { @@ -268,9 +283,11 @@ export function createRenderStream< if (stream.renders.length === 0) { await stream.waitForNextRender() } + await effectsFlushed const origRerender = ret.rerender ret.rerender = async function rerender(rerenderUi: React.ReactNode) { const previousRenderCount = stream.renders.length + const rerenderEffectsFlushed = nextEffectsFlushed() try { return await origRerender(rerenderUi) } finally { @@ -279,6 +296,7 @@ export function createRenderStream< if (previousRenderCount === stream.renders.length) { await stream.waitForNextRender() } + await rerenderEffectsFlushed } } return ret