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 (take 2) #2216

Merged
merged 29 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
ad9af11
Update React and React-DOM to the new rc version
aryaemami59 May 10, 2024
0aababf
Set `@types/react` and `@types/react-dom` to temporary types packages
aryaemami59 May 10, 2024
3e6428f
Run tests against different versions of React during CI
aryaemami59 May 10, 2024
355bbfb
Remove the now deprecated `react-test-renderer` package
aryaemami59 May 10, 2024
e69306b
Replace the now removed `ReactDOM` methods
aryaemami59 May 10, 2024
48ef75f
Replace `@testing-library/react-hooks` with `@testing-library/react`
aryaemami59 May 10, 2024
7d8dd84
Update `react-is` implementation
aryaemami59 May 10, 2024
0985471
Export `IS_REACT_19` and re-use in tests
aryaemami59 May 10, 2024
8f9ec75
Update `wrapper` `props` in `useDispatch.spec.tsx` to resolve type error
aryaemami59 May 10, 2024
a3ef256
Remove unnecessary `rtl.cleanup` calls
aryaemami59 May 10, 2024
aaa1927
Add `@ts-ignore` for type issue related to `@types/react` ^18.61
aryaemami59 May 10, 2024
5df69fc
Uncomment type test
aryaemami59 May 10, 2024
0b5c116
Add TODO comment about different rendering behaviors in React 18 vs 19
aryaemami59 May 10, 2024
dc018a7
Update `react` and `@types/react` in `peerDependencies`
aryaemami59 May 10, 2024
a92e5f2
Bump `use-sync-external-store` to the new rc
aryaemami59 May 14, 2024
834f70b
Use `types-use-sync-external-store` for `@types/use-sync-external-store`
aryaemami59 May 14, 2024
2de88c0
Fix skipped tests in `ssr.spec.tsx`
aryaemami59 May 16, 2024
b9846b0
Change `.toHaveBeenCalledTimes(0)` to `.not.toHaveBeenCalled()`
aryaemami59 May 16, 2024
7d48677
Change `.toHaveBeenCalledTimes(1)` to `.toHaveBeenCalledOnce()`
aryaemami59 May 16, 2024
86779ff
Fix duplicate `React` import in `hoistStatics.ts`
aryaemami59 May 19, 2024
deda394
Bump `@testing-library/dom` to version 10.4.0
aryaemami59 Jun 26, 2024
5cb1e0a
Bump `@testing-library/jest-dom` to version 6.6.3
aryaemami59 Jun 26, 2024
ae6200b
Bump `jsdom` to version 25.0.1
aryaemami59 Jul 25, 2024
d0add7f
Bump `@testing-library/react` to version 16.1.0
aryaemami59 Sep 4, 2024
eaa8271
Fix skipped test in `test/integration/ssr.spec.tsx`
aryaemami59 Dec 6, 2024
a1fc886
Fix type of `innerMapStateToProps` in `Provider.spec.tsx`
aryaemami59 Dec 6, 2024
1cd1385
Update `hoist-non-react-statics` implementation
aryaemami59 Dec 6, 2024
e08518a
Fix `act` related issues in `test/hooks/useSelector.spec.tsx`
aryaemami59 Dec 6, 2024
c58e397
Migrate to React 19
aryaemami59 Dec 6, 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: '^19',
types: '^19',
react-dom: { version: '^19', types: '^19' },
},
]

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: '^19',
types: '^19',
react-dom: { version: '^19', types: '^19' },
},
]

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
23 changes: 11 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,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 @@ -65,7 +65,7 @@
},
"dependencies": {
"@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.2.2"
"use-sync-external-store": "^1.4.0"
},
"devDependencies": {
"@babel/cli": "^7.24.7",
Expand All @@ -80,13 +80,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.1.0",
"@types/node": "^20.14.2",
"@types/prop-types": "^15.7.12",
"@types/react": "18.3.3",
"@types/react-dom": "^18.3.0",
"@types/react": "^19.0.1",
"@types/react-dom": "^19.0.1",
"babel-eslint": "^10.1.0",
"codecov": "^3.8.3",
"cross-env": "^7.0.3",
Expand All @@ -96,11 +96,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",
"react-dom": "^19.0.0",
"redux": "^5.0.1",
"rimraf": "^5.0.7",
"tsup": "^8.3.5",
Expand Down
25 changes: 14 additions & 11 deletions src/utils/hoistStatics.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
// Copied directly from:
// https://github.com/mridgway/hoist-non-react-statics/blob/main/src/index.js
// https://unpkg.com/browse/@types/[email protected].1/index.d.ts
// https://unpkg.com/browse/@types/[email protected].6/index.d.ts

/**
* 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 { ForwardRefExoticComponent, MemoExoticComponent } from 'react'
import { ForwardRef, Memo, isMemo } from '../utils/react-is'

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

export type NonReactStatics<
S extends React.ComponentType<any>,
Source,
C extends {
[key: string]: true
} = {},
> = {
[key in Exclude<
keyof S,
S extends React.MemoExoticComponent<any>
keyof Source,
Source extends MemoExoticComponent<any>
? keyof typeof MEMO_STATICS | keyof C
: S extends React.ForwardRefExoticComponent<any>
: Source extends ForwardRefExoticComponent<any>
? keyof typeof FORWARD_REF_STATICS | keyof C
: keyof typeof REACT_STATICS | keyof typeof KNOWN_STATICS | keyof C
>]: S[key]
>]: Source[key]
}

const defineProperty = Object.defineProperty
Expand All @@ -89,12 +89,15 @@ const getPrototypeOf = Object.getPrototypeOf
const objectPrototype = Object.prototype

export default function hoistNonReactStatics<
T extends React.ComponentType<any>,
S extends React.ComponentType<any>,
C extends {
Target,
Source,
CustomStatic extends {
[key: string]: true
} = {},
>(targetComponent: T, sourceComponent: S): T & NonReactStatics<S, C> {
>(
targetComponent: Target,
sourceComponent: Source,
): Target & NonReactStatics<Source, CustomStatic> {
if (typeof sourceComponent !== 'string') {
// don't hoist over string (html) components

Expand Down
92 changes: 35 additions & 57 deletions src/utils/react-is.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
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/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 = /* @__PURE__ */ Symbol.for('react.element')
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_PROVIDER_TYPE = /* @__PURE__ */ Symbol.for('react.provider')
const REACT_CONSUMER_TYPE = /* @__PURE__ */ Symbol.for('react.consumer')
const REACT_CONTEXT_TYPE = /* @__PURE__ */ Symbol.for('react.context')
const REACT_SERVER_CONTEXT_TYPE = /* @__PURE__ */ Symbol.for(
'react.server_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(
Expand All @@ -31,87 +33,63 @@ 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
Loading