Skip to content

Commit eab950b

Browse files
rockindahizzyZachJW34lmiller1990
authored
fix(react18): unmount component with react18 API (#23204)
* fix(react18): unmount component with react18 API * Add null check and explicit typing per code review * update snapshots and make code more concise Co-authored-by: Zachary Williams <zachjw34@gmail.com> Co-authored-by: Lachlan Miller <lachlan.miller.1990@outlook.com>
1 parent 3d98f98 commit eab950b

File tree

8 files changed

+166
-308
lines changed

8 files changed

+166
-308
lines changed

npm/react/src/createMount.ts

+22-32
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import * as React from 'react'
2-
import ReactDOM from 'react-dom'
32
import getDisplayName from './getDisplayName'
43
import {
54
injectStylesBeforeElement,
65
getContainerEl,
76
ROOT_SELECTOR,
87
setupHooks,
98
} from '@cypress/mount-utils'
10-
import type { InternalMountOptions, InternalUnmountOptions, MountOptions, MountReturn, UnmountArgs } from './types'
9+
import type { InternalMountOptions, MountOptions, MountReturn, UnmountArgs } from './types'
1110

1211
/**
1312
* Inject custom style text or CSS file or 3rd party style resources
@@ -20,7 +19,7 @@ const injectStyles = (options: MountOptions) => {
2019
}
2120
}
2221

23-
export let lastMountedReactDom: (typeof ReactDOM) | undefined
22+
let mountCleanup: InternalMountOptions['cleanup']
2423

2524
/**
2625
* Create an `mount` function. Performs all the non-React-version specific
@@ -42,6 +41,8 @@ export const makeMountFn = (
4241
throw Error('internalMountOptions must be provided with `render` and `reactDom` parameters')
4342
}
4443

44+
mountCleanup = internalMountOptions.cleanup
45+
4546
// Get the display name property via the component constructor
4647
// @ts-ignore FIXME
4748
const componentName = getDisplayName(jsx.type, options.alias)
@@ -58,8 +59,6 @@ export const makeMountFn = (
5859
.then(() => {
5960
const reactDomToUse = internalMountOptions.reactDom
6061

61-
lastMountedReactDom = reactDomToUse
62-
6362
const el = getContainerEl()
6463

6564
if (!el) {
@@ -136,41 +135,32 @@ export const makeMountFn = (
136135
* This is designed to be consumed by `npm/react{16,17,18}`, and other React adapters,
137136
* or people writing adapters for third-party, custom adapters.
138137
*/
139-
export const makeUnmountFn = (options: UnmountArgs, internalUnmountOptions: InternalUnmountOptions) => {
138+
export const makeUnmountFn = (options: UnmountArgs) => {
140139
return cy.then(() => {
141-
return cy.get(ROOT_SELECTOR, { log: false }).then(($el) => {
142-
if (lastMountedReactDom) {
143-
internalUnmountOptions.unmount($el[0])
144-
const wasUnmounted = internalUnmountOptions.unmount($el[0])
145-
146-
if (wasUnmounted && options.log) {
147-
Cypress.log({
148-
name: 'unmount',
149-
type: 'parent',
150-
message: [options.boundComponentMessage ?? 'Unmounted component'],
151-
consoleProps: () => {
152-
return {
153-
description: 'Unmounts React component',
154-
parent: $el[0],
155-
home: 'https://github.com/cypress-io/cypress',
156-
}
157-
},
158-
})
159-
}
160-
}
161-
})
140+
const wasUnmounted = mountCleanup?.()
141+
142+
if (wasUnmounted && options.log) {
143+
Cypress.log({
144+
name: 'unmount',
145+
type: 'parent',
146+
message: [options.boundComponentMessage ?? 'Unmounted component'],
147+
consoleProps: () => {
148+
return {
149+
description: 'Unmounts React component',
150+
parent: getContainerEl().parentNode,
151+
home: 'https://github.com/cypress-io/cypress',
152+
}
153+
},
154+
})
155+
}
162156
})
163157
}
164158

165159
// Cleanup before each run
166160
// NOTE: we cannot use unmount here because
167161
// we are not in the context of a test
168162
const preMountCleanup = () => {
169-
const el = getContainerEl()
170-
171-
if (el && lastMountedReactDom) {
172-
lastMountedReactDom.unmountComponentAtNode(el)
173-
}
163+
mountCleanup?.()
174164
}
175165

176166
const _mount = (jsx: React.ReactNode, options: MountOptions = {}) => makeMountFn('mount', jsx, options)

npm/react/src/mount.ts

+20-10
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,44 @@
1+
import { getContainerEl } from '@cypress/mount-utils'
12
import React from 'react'
23
import ReactDOM from 'react-dom'
34
import {
45
makeMountFn,
56
makeUnmountFn,
6-
lastMountedReactDom,
77
} from './createMount'
88
import type {
99
MountOptions,
1010
InternalMountOptions,
11-
InternalUnmountOptionsReact,
1211
} from './types'
1312

13+
let lastReactDom: typeof ReactDOM
14+
15+
const cleanup = () => {
16+
if (lastReactDom) {
17+
const root = getContainerEl()
18+
19+
lastReactDom.unmountComponentAtNode(root)
20+
21+
return true
22+
}
23+
24+
return false
25+
}
26+
1427
export function mount (jsx: React.ReactNode, options: MountOptions = {}, rerenderKey?: string) {
1528
const internalOptions: InternalMountOptions = {
1629
reactDom: ReactDOM,
1730
render: (reactComponent: ReturnType<typeof React.createElement>, el: HTMLElement, reactDomToUse: typeof ReactDOM) => {
18-
return (reactDomToUse || ReactDOM).render(reactComponent, el)
31+
lastReactDom = (reactDomToUse || ReactDOM)
32+
33+
return lastReactDom.render(reactComponent, el)
1934
},
2035
unmount,
36+
cleanup,
2137
}
2238

2339
return makeMountFn('mount', jsx, { ReactDom: ReactDOM, ...options }, rerenderKey, internalOptions)
2440
}
2541

2642
export function unmount (options = { log: true }) {
27-
const internalOptions: InternalUnmountOptionsReact = {
28-
unmount: (el) => {
29-
return (lastMountedReactDom || ReactDOM).unmountComponentAtNode(el)
30-
},
31-
}
32-
33-
return makeUnmountFn(options, internalOptions)
43+
return makeUnmountFn(options)
3444
}

npm/react/src/types.ts

+1-12
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,6 @@ export interface UnmountArgs {
66
boundComponentMessage?: string
77
}
88

9-
export interface InternalUnmountOptionsReact {
10-
unmount: (el: HTMLElement) => boolean
11-
}
12-
13-
export interface InternalUnmountOptionsReact18 {
14-
unmount: () => boolean
15-
}
16-
17-
export type InternalUnmountOptions =
18-
InternalUnmountOptionsReact
19-
| InternalUnmountOptionsReact18
20-
219
export type MountOptions = Partial<StyleOptions & MountReactComponentOptions>
2210

2311
export interface MountReactComponentOptions {
@@ -43,6 +31,7 @@ export interface InternalMountOptions {
4331
reactDomToUse: typeof import('react-dom')
4432
) => void
4533
unmount: (options: UnmountArgs) => void
34+
cleanup: () => boolean
4635

4736
// globalThis.Cypress.Chainable<MountReturn>
4837
}

npm/react18/src/index.ts

+12-11
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,21 @@ import {
88
import type {
99
MountOptions,
1010
InternalMountOptions,
11-
InternalUnmountOptionsReact18,
1211
UnmountArgs,
1312
} from '@cypress/react'
1413

1514
let root: any
1615

16+
const cleanup = () => {
17+
if (root) {
18+
root.unmount()
19+
20+
return true
21+
}
22+
23+
return false
24+
}
25+
1726
export function mount (jsx: React.ReactNode, options: MountOptions = {}, rerenderKey?: string) {
1827
const internalOptions: InternalMountOptions = {
1928
reactDom: ReactDOM,
@@ -23,20 +32,12 @@ export function mount (jsx: React.ReactNode, options: MountOptions = {}, rerende
2332
return root.render(reactComponent)
2433
},
2534
unmount,
35+
cleanup,
2636
}
2737

2838
return makeMountFn('mount', jsx, { ReactDom: ReactDOM, ...options }, rerenderKey, internalOptions)
2939
}
3040

3141
export function unmount (options: UnmountArgs = { log: true }) {
32-
const internalOptions: InternalUnmountOptionsReact18 = {
33-
// type is ReturnType<typeof ReactDOM.createRoot>
34-
unmount: (): boolean => {
35-
root.unmount()
36-
37-
return true
38-
},
39-
}
40-
41-
return makeUnmountFn(options, internalOptions)
42+
return makeUnmountFn(options)
4243
}

0 commit comments

Comments
 (0)