Skip to content

Commit

Permalink
feat: Add support for React 19 Canary (#1294)
Browse files Browse the repository at this point in the history
  • Loading branch information
eps1lon committed Apr 8, 2024
1 parent 4e10ba3 commit 9c4a46d
Show file tree
Hide file tree
Showing 8 changed files with 96 additions and 46 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/validate.yml
Expand Up @@ -30,7 +30,7 @@ jobs:
fail-fast: false
matrix:
node: [14, 16, 18]
react: [latest, canary, experimental]
react: ['18.x', latest, canary, experimental]
runs-on: ubuntu-latest
steps:
- name: 猬囷笍 Checkout repo
Expand Down
15 changes: 9 additions & 6 deletions jest.config.js
Expand Up @@ -3,14 +3,17 @@ const {jest: jestConfig} = require('kcd-scripts/config')
module.exports = Object.assign(jestConfig, {
coverageThreshold: {
...jestConfig.coverageThreshold,
// Full coverage across the build matrix (React versions) but not in a single job
// Full coverage across the build matrix (React 18, 19) but not in a single job
// Ful coverage is checked via codecov
'./src/pure.js': {
// minimum coverage of jobs using different React versions
branches: 97,
'./src/act-compat': {
branches: 90,
},
'./src/pure': {
// minimum coverage of jobs using React 18 and 19
branches: 95,
functions: 88,
lines: 94,
statements: 94,
lines: 92,
statements: 92,
},
},
})
13 changes: 8 additions & 5 deletions src/__tests__/new-act.js
@@ -1,10 +1,13 @@
let asyncAct

jest.mock('react-dom/test-utils', () => ({
act: cb => {
return cb()
},
}))
jest.mock('react', () => {
return {
...jest.requireActual('react'),
act: cb => {
return cb()
},
}
})

beforeEach(() => {
jest.resetModules()
Expand Down
48 changes: 29 additions & 19 deletions src/__tests__/render.js
Expand Up @@ -3,12 +3,11 @@ import ReactDOM from 'react-dom'
import ReactDOMServer from 'react-dom/server'
import {fireEvent, render, screen, configure} from '../'

// Needs to be changed to 19.0.0 once alpha started.
const isReactExperimental = React.version.startsWith('18.3.0-experimental')
const isReactCanary = React.version.startsWith('18.3.0-canary')
const isReact18 = React.version.startsWith('18.')
const isReact19 = React.version.startsWith('19.')

// Needs to be changed to isReactExperimental || isReactCanary once alpha started.
const testGateReact18 = isReactExperimental ? test.skip : test
const testGateReact18 = isReact18 ? test : test.skip
const testGateReact19 = isReact19 ? test : test.skip

describe('render API', () => {
let originalConfig
Expand Down Expand Up @@ -224,32 +223,43 @@ describe('render API', () => {
expect(() => {
render(<div />, {legacyRoot: true})
}).toErrorDev(
isReactCanary
? [
"Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://react.dev/link/switch-to-createroot",
]
: [
"Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot",
],
[
"Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot",
],
{withoutStack: true},
)
})

testGateReact19('legacyRoot throws', () => {
expect(() => {
render(<div />, {legacyRoot: true})
}).toThrowErrorMatchingInlineSnapshot(
`\`legacyRoot: true\` is not supported in this version of React. Please use React 18 instead.`,
)
})

testGateReact18('legacyRoot uses legacy ReactDOM.hydrate', () => {
const ui = <div />
const container = document.createElement('div')
container.innerHTML = ReactDOMServer.renderToString(ui)
expect(() => {
render(ui, {container, hydrate: true, legacyRoot: true})
}).toErrorDev(
isReactCanary
? [
"Warning: ReactDOM.hydrate is no longer supported in React 18. Use hydrateRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://react.dev/link/switch-to-createroot",
]
: [
"Warning: ReactDOM.hydrate is no longer supported in React 18. Use hydrateRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot",
],
[
"Warning: ReactDOM.hydrate is no longer supported in React 18. Use hydrateRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot",
],
{withoutStack: true},
)
})

testGateReact19('legacyRoot throws even with hydrate', () => {
const ui = <div />
const container = document.createElement('div')
container.innerHTML = ReactDOMServer.renderToString(ui)
expect(() => {
render(ui, {container, hydrate: true, legacyRoot: true})
}).toThrowErrorMatchingInlineSnapshot(
`\`legacyRoot: true\` is not supported in this version of React. Please use React 18 instead.`,
)
})
})
39 changes: 27 additions & 12 deletions src/__tests__/renderHook.js
@@ -1,12 +1,11 @@
import React from 'react'
import {renderHook} from '../pure'

// Needs to be changed to 19.0.0 once alpha started.
const isReactExperimental = React.version.startsWith('18.3.0-experimental')
const isReactCanary = React.version.startsWith('18.3.0')
const isReact18 = React.version.startsWith('18.')
const isReact19 = React.version.startsWith('19.')

// Needs to be changed to isReactExperimental || isReactCanary once alpha started.
const testGateReact18 = isReactExperimental ? test.skip : test
const testGateReact18 = isReact18 ? test : test.skip
const testGateReact19 = isReact19 ? test : test.skip

test('gives committed result', () => {
const {result} = renderHook(() => {
Expand Down Expand Up @@ -85,14 +84,30 @@ testGateReact18('legacyRoot uses legacy ReactDOM.render', () => {
},
).result
}).toErrorDev(
isReactCanary
? [
"Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://react.dev/link/switch-to-createroot",
]
: [
"Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot",
],
[
"Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot",
],
{withoutStack: true},
)
expect(result.current).toEqual('provided')
})

testGateReact19('legacyRoot throws', () => {
const Context = React.createContext('default')
function Wrapper({children}) {
return <Context.Provider value="provided">{children}</Context.Provider>
}
expect(() => {
renderHook(
() => {
return React.useContext(Context)
},
{
wrapper: Wrapper,
legacyRoot: true,
},
).result
}).toThrowErrorMatchingInlineSnapshot(
`\`legacyRoot: true\` is not supported in this version of React. Please use React 18 instead.`,
)
})
7 changes: 4 additions & 3 deletions src/act-compat.js
@@ -1,6 +1,7 @@
import * as testUtils from 'react-dom/test-utils'
import * as React from 'react'
import * as DeprecatedReactTestUtils from 'react-dom/test-utils'

const domAct = testUtils.act
const reactAct = React.act ?? DeprecatedReactTestUtils.act

function getGlobalThis() {
/* istanbul ignore else */
Expand Down Expand Up @@ -78,7 +79,7 @@ function withGlobalActEnvironment(actImplementation) {
}
}

const act = withGlobalActEnvironment(domAct)
const act = withGlobalActEnvironment(reactAct)

export default act
export {
Expand Down
17 changes: 17 additions & 0 deletions src/pure.js
Expand Up @@ -207,6 +207,14 @@ function render(
wrapper,
} = {},
) {
if (legacyRoot && typeof ReactDOM.render !== 'function') {
const error = new Error(
'`legacyRoot: true` is not supported in this version of React. Please use React 18 instead.',
)
Error.captureStackTrace(error, render)
throw error
}

if (!baseElement) {
// default to document.body instead of documentElement to avoid output of potentially-large
// head elements (such as JSS style blocks) in debug output
Expand Down Expand Up @@ -263,6 +271,15 @@ function cleanup() {

function renderHook(renderCallback, options = {}) {
const {initialProps, ...renderOptions} = options

if (renderOptions.legacyRoot && typeof ReactDOM.render !== 'function') {
const error = new Error(
'`legacyRoot: true` is not supported in this version of React. Please use React 18 instead.',
)
Error.captureStackTrace(error, renderHook)
throw error
}

const result = React.createRef()

function TestComponent({renderCallbackProps}) {
Expand Down
1 change: 1 addition & 0 deletions types/index.d.ts
Expand Up @@ -73,6 +73,7 @@ export interface RenderOptions<
*/
hydrate?: boolean
/**
* Only works if used with React 18.
* Set to `true` if you want to force synchronous `ReactDOM.render`.
* Otherwise `render` will default to concurrent React if available.
*/
Expand Down

0 comments on commit 9c4a46d

Please sign in to comment.