From 69c631d7329a776556bd698cf973b9575ecf63e5 Mon Sep 17 00:00:00 2001 From: Bernardo Belchior Date: Wed, 2 Apr 2025 15:05:06 +0100 Subject: [PATCH 1/2] Add `reactStrictMode` as an option to `render` --- src/__tests__/render.js | 34 ++++++++++++++++++++++++++++++++++ src/pure.js | 18 +++++++++++------- types/index.d.ts | 5 +++++ 3 files changed, 50 insertions(+), 7 deletions(-) diff --git a/src/__tests__/render.js b/src/__tests__/render.js index f00410b4..86f614f7 100644 --- a/src/__tests__/render.js +++ b/src/__tests__/render.js @@ -262,4 +262,38 @@ describe('render API', () => { `\`legacyRoot: true\` is not supported in this version of React. If your app runs React 19 or later, you should remove this flag. If your app runs React 18 or earlier, visit https://react.dev/blog/2022/03/08/react-18-upgrade-guide for upgrade instructions.`, ) }) + + test('reactStrictMode in renderOptions has precedence over config when rendering', () => { + const wrapperComponentMountEffect = jest.fn() + function WrapperComponent({children}) { + React.useEffect(() => { + wrapperComponentMountEffect() + }) + + return children + } + const ui =
+ configure({reactStrictMode: false}) + + render(ui, {wrapper: WrapperComponent, reactStrictMode: true}) + + expect(wrapperComponentMountEffect).toHaveBeenCalledTimes(2); + }) + + test('reactStrictMode in config is used when renderOptions does not specify reactStrictMode', () => { + const wrapperComponentMountEffect = jest.fn() + function WrapperComponent({children}) { + React.useEffect(() => { + wrapperComponentMountEffect() + }) + + return children + } + const ui =
+ configure({reactStrictMode: true}) + + render(ui, {wrapper: WrapperComponent}) + + expect(wrapperComponentMountEffect).toHaveBeenCalledTimes(2); + }) }) diff --git a/src/pure.js b/src/pure.js index fe95024a..300a8255 100644 --- a/src/pure.js +++ b/src/pure.js @@ -77,8 +77,8 @@ const mountedContainers = new Set() */ const mountedRootEntries = [] -function strictModeIfNeeded(innerElement) { - return getConfig().reactStrictMode +function strictModeIfNeeded(innerElement, reactStrictMode) { + return reactStrictMode ?? getConfig().reactStrictMode ? React.createElement(React.StrictMode, null, innerElement) : innerElement } @@ -91,14 +91,14 @@ function wrapUiIfNeeded(innerElement, wrapperComponent) { function createConcurrentRoot( container, - {hydrate, onCaughtError, onRecoverableError, ui, wrapper: WrapperComponent}, + {hydrate, onCaughtError, onRecoverableError, ui, wrapper: WrapperComponent, reactStrictMode}, ) { let root if (hydrate) { act(() => { root = ReactDOMClient.hydrateRoot( container, - strictModeIfNeeded(wrapUiIfNeeded(ui, WrapperComponent)), + strictModeIfNeeded(wrapUiIfNeeded(ui, WrapperComponent), reactStrictMode), {onCaughtError, onRecoverableError}, ) }) @@ -144,17 +144,17 @@ function createLegacyRoot(container) { function renderRoot( ui, - {baseElement, container, hydrate, queries, root, wrapper: WrapperComponent}, + {baseElement, container, hydrate, queries, root, wrapper: WrapperComponent, reactStrictMode}, ) { act(() => { if (hydrate) { root.hydrate( - strictModeIfNeeded(wrapUiIfNeeded(ui, WrapperComponent)), + strictModeIfNeeded(wrapUiIfNeeded(ui, WrapperComponent), reactStrictMode), container, ) } else { root.render( - strictModeIfNeeded(wrapUiIfNeeded(ui, WrapperComponent)), + strictModeIfNeeded(wrapUiIfNeeded(ui, WrapperComponent), reactStrictMode), container, ) } @@ -180,6 +180,7 @@ function renderRoot( baseElement, root, wrapper: WrapperComponent, + reactStrictMode, }) // Intentionally do not return anything to avoid unnecessarily complicating the API. // folks can use all the same utilities we return in the first place that are bound to the container @@ -212,6 +213,7 @@ function render( queries, hydrate = false, wrapper, + reactStrictMode, } = {}, ) { if (onUncaughtError !== undefined) { @@ -248,6 +250,7 @@ function render( onRecoverableError, ui, wrapper, + reactStrictMode, }) mountedRootEntries.push({container, root}) @@ -273,6 +276,7 @@ function render( hydrate, wrapper, root, + reactStrictMode, }) } diff --git a/types/index.d.ts b/types/index.d.ts index 2f814a6d..bdd60567 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -156,6 +156,11 @@ export interface RenderOptions< * @see https://testing-library.com/docs/react-testing-library/api/#wrapper */ wrapper?: React.JSXElementConstructor<{children: React.ReactNode}> | undefined + /** + * When enabled, is rendered around the inner element. + * If defined, overrides the value of `reactStrictMode` set in `configure`. + */ + reactStrictMode?: boolean } type Omit = Pick> From 5be1d35a39f46715482f635bfdeb2fa8c0d053dd Mon Sep 17 00:00:00 2001 From: Bernardo Belchior Date: Wed, 2 Apr 2025 15:57:18 +0100 Subject: [PATCH 2/2] Fix CI --- src/__tests__/render.js | 4 ++-- src/__tests__/renderHook.js | 32 ++++++++++++++++++++++++++++++-- src/pure.js | 34 +++++++++++++++++++++++++++++----- 3 files changed, 61 insertions(+), 9 deletions(-) diff --git a/src/__tests__/render.js b/src/__tests__/render.js index 86f614f7..6f5b5b39 100644 --- a/src/__tests__/render.js +++ b/src/__tests__/render.js @@ -277,7 +277,7 @@ describe('render API', () => { render(ui, {wrapper: WrapperComponent, reactStrictMode: true}) - expect(wrapperComponentMountEffect).toHaveBeenCalledTimes(2); + expect(wrapperComponentMountEffect).toHaveBeenCalledTimes(2) }) test('reactStrictMode in config is used when renderOptions does not specify reactStrictMode', () => { @@ -294,6 +294,6 @@ describe('render API', () => { render(ui, {wrapper: WrapperComponent}) - expect(wrapperComponentMountEffect).toHaveBeenCalledTimes(2); + expect(wrapperComponentMountEffect).toHaveBeenCalledTimes(2) }) }) diff --git a/src/__tests__/renderHook.js b/src/__tests__/renderHook.js index fe7551a2..f331e90e 100644 --- a/src/__tests__/renderHook.js +++ b/src/__tests__/renderHook.js @@ -1,5 +1,5 @@ -import React from 'react' -import {renderHook} from '../pure' +import React, {useEffect} from 'react' +import {configure, renderHook} from '../pure' const isReact18 = React.version.startsWith('18.') const isReact19 = React.version.startsWith('19.') @@ -111,3 +111,31 @@ testGateReact19('legacyRoot throws', () => { `\`legacyRoot: true\` is not supported in this version of React. If your app runs React 19 or later, you should remove this flag. If your app runs React 18 or earlier, visit https://react.dev/blog/2022/03/08/react-18-upgrade-guide for upgrade instructions.`, ) }) + +describe('reactStrictMode', () => { + let originalConfig + beforeEach(() => { + // Grab the existing configuration so we can restore + // it at the end of the test + configure(existingConfig => { + originalConfig = existingConfig + // Don't change the existing config + return {} + }) + }) + + afterEach(() => { + configure(originalConfig) + }) + + test('reactStrictMode in renderOptions has precedence over config when rendering', () => { + const hookMountEffect = jest.fn() + configure({reactStrictMode: false}) + + renderHook(() => useEffect(() => hookMountEffect()), { + reactStrictMode: true, + }) + + expect(hookMountEffect).toHaveBeenCalledTimes(2) + }) +}) diff --git a/src/pure.js b/src/pure.js index 300a8255..0f9c487d 100644 --- a/src/pure.js +++ b/src/pure.js @@ -91,14 +91,24 @@ function wrapUiIfNeeded(innerElement, wrapperComponent) { function createConcurrentRoot( container, - {hydrate, onCaughtError, onRecoverableError, ui, wrapper: WrapperComponent, reactStrictMode}, + { + hydrate, + onCaughtError, + onRecoverableError, + ui, + wrapper: WrapperComponent, + reactStrictMode, + }, ) { let root if (hydrate) { act(() => { root = ReactDOMClient.hydrateRoot( container, - strictModeIfNeeded(wrapUiIfNeeded(ui, WrapperComponent), reactStrictMode), + strictModeIfNeeded( + wrapUiIfNeeded(ui, WrapperComponent), + reactStrictMode, + ), {onCaughtError, onRecoverableError}, ) }) @@ -144,17 +154,31 @@ function createLegacyRoot(container) { function renderRoot( ui, - {baseElement, container, hydrate, queries, root, wrapper: WrapperComponent, reactStrictMode}, + { + baseElement, + container, + hydrate, + queries, + root, + wrapper: WrapperComponent, + reactStrictMode, + }, ) { act(() => { if (hydrate) { root.hydrate( - strictModeIfNeeded(wrapUiIfNeeded(ui, WrapperComponent), reactStrictMode), + strictModeIfNeeded( + wrapUiIfNeeded(ui, WrapperComponent), + reactStrictMode, + ), container, ) } else { root.render( - strictModeIfNeeded(wrapUiIfNeeded(ui, WrapperComponent), reactStrictMode), + strictModeIfNeeded( + wrapUiIfNeeded(ui, WrapperComponent), + reactStrictMode, + ), container, ) }