Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate to React 19 #2172

Draft
wants to merge 25 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
fa5a606
Update React and React-DOM to the new rc version
aryaemami59 May 10, 2024
ae5bc1c
Set `@types/react` and `@types/react-dom` to temporary types packages
aryaemami59 May 10, 2024
7b0f265
Run tests against different versions of React during CI
aryaemami59 May 10, 2024
4974788
Remove the now deprecated `react-test-renderer` package
aryaemami59 May 10, 2024
478c92b
Replace the now removed `ReactDOM` methods
aryaemami59 May 10, 2024
9a319a4
Replace `@testing-library/react-hooks` with `@testing-library/react`
aryaemami59 May 10, 2024
cf93795
Update `react-is` implementation
aryaemami59 May 10, 2024
7fcc65f
Export `IS_REACT_19` and re-use in tests
aryaemami59 May 10, 2024
7f28716
Update `wrapper` `props` in `useDispatch.spec.tsx` to resolve type error
aryaemami59 May 10, 2024
9a672f5
Remove unnecessary `rtl.cleanup` calls
aryaemami59 May 10, 2024
827daba
Add `@ts-ignore` for type issue related to `@types/react` ^18.61
aryaemami59 May 10, 2024
676b1fe
Uncomment type test
aryaemami59 May 10, 2024
bbd7fc8
Add TODO comment about different rendering behaviors in React 18 vs 19
aryaemami59 May 10, 2024
33a5ac5
Update `react` and `@types/react` in `peerDependencies`
aryaemami59 May 10, 2024
fa04a52
Bump `use-sync-external-store` to the new rc
aryaemami59 May 14, 2024
338c82e
Use `types-use-sync-external-store` for `@types/use-sync-external-store`
aryaemami59 May 14, 2024
11934c2
Fix skipped tests in `ssr.spec.tsx`
aryaemami59 May 16, 2024
2988d21
Fix React 18 ssr test
aryaemami59 May 16, 2024
a37665e
Change `.toHaveBeenCalledTimes(0)` to `.not.toHaveBeenCalled()`
aryaemami59 May 16, 2024
439030f
Change `.toHaveBeenCalledTimes(1)` to `.toHaveBeenCalledOnce()`
aryaemami59 May 16, 2024
f05bf2f
Fix duplicate `React` import in `hoistStatics.ts`
aryaemami59 May 19, 2024
5acc575
Bump `@testing-library/dom` to version 10.4.0
aryaemami59 Jun 26, 2024
e0701ac
Bump `@testing-library/jest-dom` to version 6.6.3
aryaemami59 Jun 26, 2024
f80d281
Bump `jsdom` to version 25.0.1
aryaemami59 Jul 25, 2024
5f46b99
Bump `@testing-library/react` to version 16.0.1
aryaemami59 Sep 4, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 35 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
path: ./package.tgz

test-types:
name: Test Types with TypeScript ${{ matrix.ts }}
name: Test Types with TypeScript ${{ matrix.ts }} and React ${{ matrix.react.version }}

needs: [build]
runs-on: ubuntu-latest
Expand All @@ -47,6 +47,19 @@ jobs:
matrix:
node: ['20.x']
ts: ['4.7', '4.8', '4.9', '5.0', '5.1', '5.2', '5.3', '5.4', '5.5']
react:
[
{
version: '^18',
types: ^18,
react-dom: { version: '^18', types: '^18' },
},
{
version: 'rc',
types: 'npm:types-react@rc',
react-dom: { version: 'rc', types: 'npm:types-react-dom@rc' },
},
]

steps:
- name: Checkout repo
Expand All @@ -67,6 +80,9 @@ jobs:
- name: Install deps
run: yarn install

- name: Install React ${{ matrix.react.version }} and React-DOM ${{ matrix.react.react-dom.version }}
run: yarn add -D react@${{ matrix.react.version }} react-dom@${{ matrix.react.react-dom.version }} @types/react@${{ matrix.react.types }} @types/react-dom@${{ matrix.react.react-dom.types }}

- name: Install TypeScript ${{ matrix.ts }}
run: yarn add typescript@${{ matrix.ts }}

Expand Down Expand Up @@ -230,13 +246,27 @@ jobs:
run: yarn build

test-dist:
name: Run local tests against build artifact
name: Run local tests against build artifact (React ${{ matrix.react.version }})
needs: [build]
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node: ['20.x']
react:
[
{
version: '^18',
types: ^18,
react-dom: { version: '^18', types: '^18' },
},
{
version: 'rc',
types: 'npm:types-react@rc',
react-dom: { version: 'rc', types: 'npm:types-react-dom@rc' },
},
]

steps:
- name: Checkout repo
uses: actions/checkout@v4
Expand All @@ -259,6 +289,9 @@ jobs:
- name: Check folder contents
run: ls -lah

- name: Install React ${{ matrix.react.version }} and React-DOM ${{ matrix.react.react-dom.version }}
run: yarn add -D react@${{ matrix.react.version }} react-dom@${{ matrix.react.react-dom.version }} @types/react@${{ matrix.react.types }} @types/react-dom@${{ matrix.react.react-dom.types }}

- name: Install build artifact
run: yarn add ./package.tgz

Expand Down
25 changes: 12 additions & 13 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@
"coverage": "codecov"
},
"peerDependencies": {
"@types/react": "^18.2.25",
"react": "^18.0",
"@types/react": "^18.2.25 || ^19",
"react": "^18.0 || ^19",
"redux": "^5.0.0"
},
"peerDependenciesMeta": {
Expand All @@ -63,8 +63,8 @@
}
},
"dependencies": {
"@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.2.2"
"@types/use-sync-external-store": "npm:types-use-sync-external-store@^19.0.0-rc",
"use-sync-external-store": "^1.2.2 || ^1.4.0-rc"
},
"devDependencies": {
"@babel/cli": "^7.24.7",
Expand All @@ -79,13 +79,13 @@
"@babel/preset-typescript": "^7.24.7",
"@microsoft/api-extractor": "^7.47.0",
"@reduxjs/toolkit": "^2.2.5",
"@testing-library/dom": "^10.1.0",
"@testing-library/jest-dom": "^6.4.5",
"@testing-library/react": "^16.0.0",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.0.1",
"@types/node": "^20.14.2",
"@types/prop-types": "^15.7.12",
"@types/react": "18.3.3",
"@types/react-dom": "^18.3.0",
"@types/react": "npm:types-react@^19.0.0-rc",
"@types/react-dom": "npm:types-react-dom@^19.0.0-rc",
"babel-eslint": "^10.1.0",
"codecov": "^3.8.3",
"cross-env": "^7.0.3",
Expand All @@ -95,11 +95,10 @@
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-react": "^7.34.2",
"jsdom": "^24.1.0",
"jsdom": "^25.0.1",
"prettier": "^3.3.3",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-test-renderer": "18.3.1",
"react": "^19.0.0-rc",
"react-dom": "^19.0.0-rc",
"redux": "^5.0.1",
"rimraf": "^5.0.7",
"tsup": "7.0.0",
Expand Down
16 changes: 10 additions & 6 deletions src/utils/hoistStatics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@
* Copyright 2015, Yahoo! Inc.
* Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
import type * as React from 'react'
import type {
ComponentType,
ForwardRefExoticComponent,
MemoExoticComponent,
} from 'react'
import { ForwardRef, Memo, isMemo } from '../utils/react-is'

const REACT_STATICS = {
Expand Down Expand Up @@ -66,16 +70,16 @@ function getStatics(component: any) {
}

export type NonReactStatics<
S extends React.ComponentType<any>,
S extends ComponentType<any>,
C extends {
[key: string]: true
} = {},
> = {
[key in Exclude<
keyof S,
S extends React.MemoExoticComponent<any>
S extends MemoExoticComponent<any>
? keyof typeof MEMO_STATICS | keyof C
: S extends React.ForwardRefExoticComponent<any>
: S extends ForwardRefExoticComponent<any>
? keyof typeof FORWARD_REF_STATICS | keyof C
: keyof typeof REACT_STATICS | keyof typeof KNOWN_STATICS | keyof C
>]: S[key]
Expand All @@ -89,8 +93,8 @@ const getPrototypeOf = Object.getPrototypeOf
const objectPrototype = Object.prototype

export default function hoistNonReactStatics<
T extends React.ComponentType<any>,
S extends React.ComponentType<any>,
T extends ComponentType<any>,
S extends ComponentType<any>,
C extends {
[key: string]: true
} = {},
Expand Down
118 changes: 51 additions & 67 deletions src/utils/react-is.ts
Original file line number Diff line number Diff line change
@@ -1,111 +1,95 @@
import type { ElementType, MemoExoticComponent, ReactElement } from 'react'
import * as React from 'react'

// Directly ported from:
// https://unpkg.com/browse/react-is@18.3.0-canary-ee68446ff-20231115/cjs/react-is.production.js
// https://unpkg.com/browse/react-is@19.0.0-rc.0/cjs/react-is.production.js
// It's very possible this could change in the future, but given that
// we only use these in `connect`, this is a low priority.

const REACT_ELEMENT_TYPE = Symbol.for('react.element')
const REACT_PORTAL_TYPE = Symbol.for('react.portal')
const REACT_FRAGMENT_TYPE = Symbol.for('react.fragment')
const REACT_STRICT_MODE_TYPE = Symbol.for('react.strict_mode')
const REACT_PROFILER_TYPE = Symbol.for('react.profiler')
const REACT_PROVIDER_TYPE = Symbol.for('react.provider')
const REACT_CONTEXT_TYPE = Symbol.for('react.context')
const REACT_SERVER_CONTEXT_TYPE = Symbol.for('react.server_context')
const REACT_FORWARD_REF_TYPE = Symbol.for('react.forward_ref')
const REACT_SUSPENSE_TYPE = Symbol.for('react.suspense')
const REACT_SUSPENSE_LIST_TYPE = Symbol.for('react.suspense_list')
const REACT_MEMO_TYPE = Symbol.for('react.memo')
const REACT_LAZY_TYPE = Symbol.for('react.lazy')
const REACT_OFFSCREEN_TYPE = Symbol.for('react.offscreen')
const REACT_CLIENT_REFERENCE = Symbol.for('react.client.reference')
export const IS_REACT_19 = /* @__PURE__ */ React.version.startsWith('19')

const REACT_ELEMENT_TYPE = /* @__PURE__ */ Symbol.for(
IS_REACT_19 ? 'react.transitional.element' : 'react.element',
)
const REACT_PORTAL_TYPE = /* @__PURE__ */ Symbol.for('react.portal')
const REACT_FRAGMENT_TYPE = /* @__PURE__ */ Symbol.for('react.fragment')
const REACT_STRICT_MODE_TYPE = /* @__PURE__ */ Symbol.for('react.strict_mode')
const REACT_PROFILER_TYPE = /* @__PURE__ */ Symbol.for('react.profiler')
const REACT_CONSUMER_TYPE = /* @__PURE__ */ Symbol.for('react.consumer')
const REACT_CONTEXT_TYPE = /* @__PURE__ */ Symbol.for('react.context')
const REACT_FORWARD_REF_TYPE = /* @__PURE__ */ Symbol.for('react.forward_ref')
const REACT_SUSPENSE_TYPE = /* @__PURE__ */ Symbol.for('react.suspense')
const REACT_SUSPENSE_LIST_TYPE = /* @__PURE__ */ Symbol.for(
'react.suspense_list',
)
const REACT_MEMO_TYPE = /* @__PURE__ */ Symbol.for('react.memo')
const REACT_LAZY_TYPE = /* @__PURE__ */ Symbol.for('react.lazy')
const REACT_OFFSCREEN_TYPE = /* @__PURE__ */ Symbol.for('react.offscreen')
const REACT_CLIENT_REFERENCE = /* @__PURE__ */ Symbol.for(
'react.client.reference',
)

export const ForwardRef = REACT_FORWARD_REF_TYPE
export const Memo = REACT_MEMO_TYPE

export function isValidElementType(type: any): type is ElementType {
if (typeof type === 'string' || typeof type === 'function') {
return true
} // Note: typeof might be other than 'symbol' or 'number' (e.g. if it's a polyfill).

if (
return typeof type === 'string' ||
typeof type === 'function' ||
type === REACT_FRAGMENT_TYPE ||
type === REACT_PROFILER_TYPE ||
type === REACT_STRICT_MODE_TYPE ||
type === REACT_SUSPENSE_TYPE ||
type === REACT_SUSPENSE_LIST_TYPE ||
type === REACT_OFFSCREEN_TYPE
) {
return true
}

if (typeof type === 'object' && type !== null) {
if (
type.$$typeof === REACT_LAZY_TYPE ||
type.$$typeof === REACT_MEMO_TYPE ||
type.$$typeof === REACT_PROVIDER_TYPE ||
type.$$typeof === REACT_CONTEXT_TYPE ||
type.$$typeof === REACT_FORWARD_REF_TYPE || // This needs to include all possible module reference object
// types supported by any Flight configuration anywhere since
// we don't know which Flight build this will end up being used
// with.
type.$$typeof === REACT_CLIENT_REFERENCE ||
type.getModuleId !== undefined
) {
return true
}
}

return false
type === REACT_OFFSCREEN_TYPE ||
(typeof type === 'object' &&
type !== null &&
(type.$$typeof === REACT_LAZY_TYPE ||
type.$$typeof === REACT_MEMO_TYPE ||
type.$$typeof === REACT_CONTEXT_TYPE ||
type.$$typeof === REACT_CONSUMER_TYPE ||
type.$$typeof === REACT_FORWARD_REF_TYPE ||
type.$$typeof === REACT_CLIENT_REFERENCE ||
type.getModuleId !== undefined))
? !0
: !1
}

function typeOf(object: any): symbol | undefined {
if (typeof object === 'object' && object !== null) {
const $$typeof = object.$$typeof
const { $$typeof } = object

switch ($$typeof) {
case REACT_ELEMENT_TYPE: {
const type = object.type

switch (type) {
case REACT_ELEMENT_TYPE:
switch (((object = object.type), object)) {
case REACT_FRAGMENT_TYPE:
case REACT_PROFILER_TYPE:
case REACT_STRICT_MODE_TYPE:
case REACT_SUSPENSE_TYPE:
case REACT_SUSPENSE_LIST_TYPE:
return type

default: {
const $$typeofType = type && type.$$typeof

switch ($$typeofType) {
case REACT_SERVER_CONTEXT_TYPE:
return object
default:
switch (((object = object && object.$$typeof), object)) {
case REACT_CONTEXT_TYPE:
case REACT_FORWARD_REF_TYPE:
case REACT_LAZY_TYPE:
case REACT_MEMO_TYPE:
case REACT_PROVIDER_TYPE:
return $$typeofType

return object
case REACT_CONSUMER_TYPE:
return object
default:
return $$typeof
}
}
}
}

case REACT_PORTAL_TYPE: {
case REACT_PORTAL_TYPE:
return $$typeof
}
}
}

return undefined
}

export function isContextConsumer(object: any): object is ReactElement {
return typeOf(object) === REACT_CONTEXT_TYPE
return IS_REACT_19
? typeOf(object) === REACT_CONSUMER_TYPE
: typeOf(object) === REACT_CONTEXT_TYPE
}

export function isMemo(object: any): object is MemoExoticComponent<any> {
Expand Down
Loading