From ae449f5d2c4d2c5f122e96a931c2f59f4fbc7b4c Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Thu, 7 Aug 2025 23:23:15 +0200 Subject: [PATCH 1/6] tweaks --- src/render-hook.tsx | 55 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 43 insertions(+), 12 deletions(-) diff --git a/src/render-hook.tsx b/src/render-hook.tsx index 4cc67ac5..1720071f 100644 --- a/src/render-hook.tsx +++ b/src/render-hook.tsx @@ -1,13 +1,20 @@ import * as React from 'react'; -import { renderInternal } from './render'; +import render from './render'; +import renderAsync from './render-async'; export type RenderHookResult = { + result: React.RefObject; rerender: (props: Props) => void; - result: React.MutableRefObject; unmount: () => void; }; +export type RenderHookAsyncResult = { + result: React.RefObject; + rerender: (props: Props) => Promise; + unmount: () => Promise; +}; + export type RenderHookOptions = { /** * The initial props to pass to the hook. @@ -32,13 +39,40 @@ export function renderHook( hookToRender: (props: Props) => Result, options?: RenderHookOptions, ): RenderHookResult { + const result: React.RefObject = React.createRef(); + + function HookContainer({ hookProps }: { hookProps: Props }) { + const renderResult = hookToRender(hookProps); + React.useEffect(() => { + result.current = renderResult; + }); + + return null; + } + const { initialProps, ...renderOptions } = options ?? {}; + const { rerender: rerenderComponent, unmount } = render( + // @ts-expect-error since option can be undefined, initialProps can be undefined when it should'nt + , + renderOptions, + ); - const result: React.MutableRefObject = React.createRef(); + return { + // Result should already be set after the first render effects are run. + result: result as React.RefObject, + rerender: (hookProps: Props) => rerenderComponent(), + unmount, + }; +} + +export async function renderHookAsync( + hookToRender: (props: Props) => Result, + options?: RenderHookOptions, +): Promise> { + const result: React.RefObject = React.createRef(); function TestComponent({ hookProps }: { hookProps: Props }) { const renderResult = hookToRender(hookProps); - React.useEffect(() => { result.current = renderResult; }); @@ -46,20 +80,17 @@ export function renderHook( return null; } - const { rerender: componentRerender, unmount } = renderInternal( + const { initialProps, ...renderOptions } = options ?? {}; + const { rerenderAsync: rerenderComponentAsync, unmountAsync } = await renderAsync( // @ts-expect-error since option can be undefined, initialProps can be undefined when it should'nt , renderOptions, ); - function rerender(hookProps: Props) { - return componentRerender(); - } - return { // Result should already be set after the first render effects are run. - result: result as React.MutableRefObject, - rerender, - unmount, + result: result as React.RefObject, + rerender: (hookProps: Props) => rerenderComponentAsync(), + unmount: unmountAsync, }; } From 6f37bf9d0c5eac414d05bf41cf7060621653e609 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Thu, 7 Aug 2025 23:23:30 +0200 Subject: [PATCH 2/6] tests --- src/__tests__/render-async.test.tsx | 4 +- src/__tests__/render-hook-async.test.tsx | 277 +++++++++++++++++++++++ src/pure.ts | 2 +- 3 files changed, 280 insertions(+), 3 deletions(-) create mode 100644 src/__tests__/render-hook-async.test.tsx diff --git a/src/__tests__/render-async.test.tsx b/src/__tests__/render-async.test.tsx index 8fad2cc3..f98af36b 100644 --- a/src/__tests__/render-async.test.tsx +++ b/src/__tests__/render-async.test.tsx @@ -54,8 +54,8 @@ test('renderAsync with wrapper option', async () => { expect(screen.getByTestId('inner')).toBeTruthy(); }); -test('renderAsync supports concurrent rendering option', async () => { - await renderAsync(, { concurrentRoot: true }); +test('renderAsync supports legacy rendering option', async () => { + await renderAsync(, { concurrentRoot: false }); expect(screen.root).toBeOnTheScreen(); }); diff --git a/src/__tests__/render-hook-async.test.tsx b/src/__tests__/render-hook-async.test.tsx new file mode 100644 index 00000000..f7acc6b1 --- /dev/null +++ b/src/__tests__/render-hook-async.test.tsx @@ -0,0 +1,277 @@ +import type { ReactNode } from 'react'; +import * as React from 'react'; + +import { act, renderHookAsync } from '..'; +import { excludeConsoleMessage } from '../test-utils/console'; + +const testGateReact19 = React.version.startsWith('19.') ? test : test.skip; + +// eslint-disable-next-line no-console +const originalConsoleError = console.error; +afterEach(() => { + // eslint-disable-next-line no-console + console.error = originalConsoleError; +}); + +test('renderHookAsync renders hook asynchronously', async () => { + const { result } = await renderHookAsync(() => { + const [state, setState] = React.useState(1); + + React.useEffect(() => { + setState(2); + }, []); + + return state; + }); + + expect(result.current).toEqual(2); +}); + +test('renderHookAsync with wrapper option', async () => { + const Context = React.createContext('default'); + + function useTestHook() { + return React.useContext(Context); + } + + function Wrapper({ children }: { children: ReactNode }) { + return {children}; + } + + const { result } = await renderHookAsync(useTestHook, { wrapper: Wrapper }); + expect(result.current).toEqual('provided'); +}); + +test('renderHookAsync supports legacy rendering option', async () => { + function useTestHook() { + return React.useState(42)[0]; + } + + const { result } = await renderHookAsync(useTestHook, { concurrentRoot: false }); + expect(result.current).toEqual(42); +}); + +test('rerender function updates hook asynchronously', async () => { + function useTestHook(props: { value: number }) { + const [state, setState] = React.useState(props.value); + + React.useEffect(() => { + setState(props.value * 2); + }, [props.value]); + + return state; + } + + const { result, rerender } = await renderHookAsync(useTestHook, { initialProps: { value: 5 } }); + expect(result.current).toEqual(10); + + await rerender({ value: 10 }); + expect(result.current).toEqual(20); +}); + +test('unmount function unmounts hook asynchronously', async () => { + let cleanupCalled = false; + + function useTestHook() { + React.useEffect(() => { + return () => { + cleanupCalled = true; + }; + }, []); + + return 'test'; + } + + const { unmount } = await renderHookAsync(useTestHook); + expect(cleanupCalled).toBe(false); + + await unmount(); + expect(cleanupCalled).toBe(true); +}); + +test('handles hook with state updates during effects', async () => { + function useTestHook() { + const [count, setCount] = React.useState(0); + + React.useEffect(() => { + setCount((prev) => prev + 1); + }, []); + + return count; + } + + const { result } = await renderHookAsync(useTestHook); + expect(result.current).toBe(1); +}); + +test('handles multiple state updates in effects', async () => { + function useTestHook() { + const [first, setFirst] = React.useState(1); + const [second, setSecond] = React.useState(2); + + React.useEffect(() => { + setFirst(10); + setSecond(20); + }, []); + + return { first, second }; + } + + const { result } = await renderHookAsync(useTestHook); + expect(result.current).toEqual({ first: 10, second: 20 }); +}); + +testGateReact19('handles hook with suspense', async () => { + function useSuspendingHook(promise: Promise) { + return React.use(promise); + } + + let resolvePromise: (value: string) => void; + const promise = new Promise((resolve) => { + resolvePromise = resolve; + }); + + const { result } = await renderHookAsync(useSuspendingHook, { + initialProps: promise, + wrapper: ({ children }) => {children}, + }); + + // Initially suspended, result should not be available + expect(result.current).toBeNull(); + + // eslint-disable-next-line require-await + await act(async () => resolvePromise('resolved')); + expect(result.current).toBe('resolved'); +}); + +class ErrorBoundary extends React.Component< + { children: React.ReactNode; fallback: string }, + { hasError: boolean } +> { + constructor(props: { children: React.ReactNode; fallback: string }) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError() { + return { hasError: true }; + } + + render() { + return this.state.hasError ? this.props.fallback : this.props.children; + } +} + +testGateReact19('handles hook suspense with error boundary', async () => { + const ERROR_MESSAGE = 'Hook Promise Rejected In Test'; + // eslint-disable-next-line no-console + console.error = excludeConsoleMessage(console.error, ERROR_MESSAGE); + + function useSuspendingHook(promise: Promise) { + return React.use(promise); + } + + let rejectPromise: (error: Error) => void; + const promise = new Promise((_resolve, reject) => { + rejectPromise = reject; + }); + + const { result } = await renderHookAsync(useSuspendingHook, { + initialProps: promise, + wrapper: ({ children }) => ( + + {children} + + ), + }); + + // Initially suspended + expect(result.current).toBeNull(); + + // eslint-disable-next-line require-await + await act(async () => rejectPromise(new Error(ERROR_MESSAGE))); + + // After error, result should still be null (error boundary caught it) + expect(result.current).toBeNull(); +}); + +test('handles custom hooks with complex logic', async () => { + function useCounter(initialValue: number) { + const [count, setCount] = React.useState(initialValue); + + const increment = React.useCallback(() => { + setCount((prev) => prev + 1); + }, []); + + const decrement = React.useCallback(() => { + setCount((prev) => prev - 1); + }, []); + + const reset = React.useCallback(() => { + setCount(initialValue); + }, [initialValue]); + + return { count, increment, decrement, reset }; + } + + const { result, rerender } = await renderHookAsync(useCounter, { initialProps: 5 }); + + expect(result.current.count).toBe(5); + + // Test increment + result.current.increment(); + await rerender(5); + expect(result.current.count).toBe(6); + + // Test decrement + result.current.decrement(); + await rerender(5); + expect(result.current.count).toBe(5); + + // Test reset + result.current.increment(); + result.current.increment(); + await rerender(5); + expect(result.current.count).toBe(7); + + result.current.reset(); + await rerender(5); + expect(result.current.count).toBe(5); +}); + +test('handles hook with cleanup and re-initialization', async () => { + let effectCount = 0; + let cleanupCount = 0; + + function useTestHook(props: { key: string }) { + const [value, setValue] = React.useState(props.key); + + React.useEffect(() => { + effectCount++; + setValue(`${props.key}-effect`); + + return () => { + cleanupCount++; + }; + }, [props.key]); + + return value; + } + + const { result, rerender, unmount } = await renderHookAsync(useTestHook, { + initialProps: { key: 'initial' }, + }); + + expect(result.current).toBe('initial-effect'); + expect(effectCount).toBe(1); + expect(cleanupCount).toBe(0); + + await rerender({ key: 'updated' }); + expect(result.current).toBe('updated-effect'); + expect(effectCount).toBe(2); + expect(cleanupCount).toBe(1); + + await unmount(); + expect(effectCount).toBe(2); + expect(cleanupCount).toBe(2); +}); diff --git a/src/pure.ts b/src/pure.ts index 62be84c2..4d8d83a6 100644 --- a/src/pure.ts +++ b/src/pure.ts @@ -10,7 +10,7 @@ export { within, getQueriesForElement } from './within'; export { configure, resetToDefaults } from './config'; export { isHiddenFromAccessibility, isInaccessible } from './helpers/accessibility'; export { getDefaultNormalizer } from './matches'; -export { renderHook } from './render-hook'; +export { renderHook, renderHookAsync } from './render-hook'; export { screen } from './screen'; export { userEvent } from './user-event'; From 4bf2a23a1331a6e578e2f4b71a64cafa12e73de1 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Thu, 7 Aug 2025 23:46:03 +0200 Subject: [PATCH 3/6] docs --- .../docs/13.x/docs/api/events/fire-event.mdx | 16 +++--- .../docs/13.x/docs/api/misc/render-hook.mdx | 51 ++++++++++++++++++- website/docs/13.x/docs/api/render.mdx | 4 +- website/docs/13.x/docs/api/screen.mdx | 8 +-- 4 files changed, 65 insertions(+), 14 deletions(-) diff --git a/website/docs/13.x/docs/api/events/fire-event.mdx b/website/docs/13.x/docs/api/events/fire-event.mdx index 8ce54470..c2ab6895 100644 --- a/website/docs/13.x/docs/api/events/fire-event.mdx +++ b/website/docs/13.x/docs/api/events/fire-event.mdx @@ -157,7 +157,6 @@ Prefer using [`user.scrollTo`](docs/api/events/user-event#scrollto) over `fireEv ::: - ## `fireEventAsync` :::info RNTL minimal version @@ -166,12 +165,15 @@ This API requires RNTL v13.3.0 or later. ::: - ```ts -async function fireEventAsync(element: ReactTestInstance, eventName: string, ...data: unknown[]): Promise; +async function fireEventAsync( + element: ReactTestInstance, + eventName: string, + ...data: unknown[] +): Promise; ``` -The `fireEventAsync` function is the async version of `fireEvent` designed for working with React 19 and React Suspense. It wraps event handler execution in async `act()`, making it suitable for event handlers that trigger suspense boundaries or other async behavior. +The `fireEventAsync` function is the async version of `fireEvent` designed for working with React 19 and React Suspense. This function uses async `act` function internally to ensure all pending React updates are executed during event handling. ```jsx import { renderAsync, screen, fireEventAsync } from '@testing-library/react-native'; @@ -192,7 +194,7 @@ Like `fireEvent`, `fireEventAsync` also provides convenience methods for common fireEventAsync.press: (element: ReactTestInstance, ...data: Array) => Promise ``` -Async version of `fireEvent.press` designed for React 19 and React Suspense. Use when press event handlers trigger suspense boundaries or other async behavior. +Async version of `fireEvent.press` designed for React 19 and React Suspense. Use when press event handlers trigger suspense boundaries. ### `fireEventAsync.changeText` {#async-change-text} @@ -200,7 +202,7 @@ Async version of `fireEvent.press` designed for React 19 and React Suspense. Use fireEventAsync.changeText: (element: ReactTestInstance, ...data: Array) => Promise ``` -Async version of `fireEvent.changeText` designed for React 19 and React Suspense. Use when changeText event handlers trigger suspense boundaries or other async behavior. +Async version of `fireEvent.changeText` designed for React 19 and React Suspense. Use when changeText event handlers trigger suspense boundaries. ### `fireEventAsync.scroll` {#async-scroll} @@ -208,4 +210,4 @@ Async version of `fireEvent.changeText` designed for React 19 and React Suspense fireEventAsync.scroll: (element: ReactTestInstance, ...data: Array) => Promise ``` -Async version of `fireEvent.scroll` designed for React 19 and React Suspense. Use when scroll event handlers trigger suspense boundaries or other async behavior. +Async version of `fireEvent.scroll` designed for React 19 and React Suspense. Use when scroll event handlers trigger suspense boundaries. diff --git a/website/docs/13.x/docs/api/misc/render-hook.mdx b/website/docs/13.x/docs/api/misc/render-hook.mdx index 5767aaaf..eab8e856 100644 --- a/website/docs/13.x/docs/api/misc/render-hook.mdx +++ b/website/docs/13.x/docs/api/misc/render-hook.mdx @@ -2,7 +2,7 @@ ```ts function renderHook( - callback: (props?: Props) => Result, + hookFn: (props?: Props) => Result, options?: RenderHookOptions ): RenderHookResult; ``` @@ -129,3 +129,52 @@ it('should use context value', () => { // ... }); ``` + +## `renderHookAsync` function + +```ts +async function renderHookAsync( + hookFn: (props?: Props) => Result, + options?: RenderHookOptions +): Promise>; +``` + +Async versions of `renderHook` designed for working with React 19 and React Suspense. This method uses async `act` function internally to ensure all pending React updates are executed during rendering. + +- **Returns a Promise**: Should be awaited +- **Async methods**: Both `rerender` and `unmount` return Promises and should be awaited +- **Suspense support**: Compatible with React Suspense boundaries and `React.use()` + +### Result {#result-async} + +```ts +interface RenderHookAsyncResult { + result: { current: Result }; + rerender: (props: Props) => Promise; + unmount: () => Promise; +} +``` + +The `RenderHookAsyncResult` differs from `RenderHookResult` in that `rerender` and `unmount` are async functions. + +```ts +import { renderHookAsync, act } from '@testing-library/react-native'; + +test('should handle async hook behavior', async () => { + const { result, rerender } = await renderHookAsync(useAsyncHook); + + // Test initial state + expect(result.current.loading).toBe(true); + + // Wait for async operation to complete + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 100)); + }); + + // Re-render to get updated state + await rerender(); + expect(result.current.loading).toBe(false); +}); +``` + +Use `renderHookAsync` when testing hooks that use React Suspense, `React.use()`, or other concurrent features where timing of re-renders matters. diff --git a/website/docs/13.x/docs/api/render.mdx b/website/docs/13.x/docs/api/render.mdx index 2c89591c..b7560e94 100644 --- a/website/docs/13.x/docs/api/render.mdx +++ b/website/docs/13.x/docs/api/render.mdx @@ -77,10 +77,10 @@ This API requires RNTL v13.3.0 or later. async function renderAsync( component: React.Element, options?: RenderAsyncOptions -): Promise +): Promise; ``` -The `renderAsync` function is the async version of `render` designed for working with React 19 and React Suspense. It allows components to be properly rendered when they contain suspense boundaries or async behavior that needs to complete before the render result is returned. +The `renderAsync` function is the async version of `render` designed for working with React 19 and React Suspense. This function uses async `act` function internally to ensure all pending React updates are executed during rendering. ```jsx import { renderAsync, screen } from '@testing-library/react-native'; diff --git a/website/docs/13.x/docs/api/screen.mdx b/website/docs/13.x/docs/api/screen.mdx index 06b448df..d4016d4f 100644 --- a/website/docs/13.x/docs/api/screen.mdx +++ b/website/docs/13.x/docs/api/screen.mdx @@ -55,17 +55,17 @@ This API requires RNTL v13.3.0 or later. function rerenderAsync(element: React.Element): Promise; ``` -Async versions of `rerender` designed for working with React 19 and React Suspense. These methods wait for async operations to complete during re-rendering, making them suitable for components that use suspense boundaries or other async behavior. +Async versions of `rerender` designed for working with React 19 and React Suspense. This method uses async `act` function internally to ensure all pending React updates are executed during updating. ```jsx import { renderAsync, screen } from '@testing-library/react-native'; test('async rerender test', async () => { await renderAsync(); - + // Use async rerender when component has suspense or async behavior await screen.rerenderAsync(); - + expect(screen.getByText('updated')).toBeOnTheScreen(); }); ``` @@ -96,7 +96,7 @@ This API requires RNTL v13.3.0 or later. function unmountAsync(): Promise; ``` -Async version of `unmount` designed for working with React 19 and React Suspense. This method waits for async cleanup operations to complete during unmounting, making it suitable for components that have async cleanup behavior. +Async version of `unmount` designed for working with React 19 and React Suspense. This method uses async `act` function internally to ensure all pending React updates are executed during unmounting. :::note From 108754f89dc5d141188df17234b02e4a28a55342 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Fri, 8 Aug 2025 16:10:46 +0200 Subject: [PATCH 4/6] tweaks --- src/__tests__/render-hook-async.test.tsx | 43 +++++++++--------------- src/render-hook.tsx | 8 ++--- 2 files changed, 20 insertions(+), 31 deletions(-) diff --git a/src/__tests__/render-hook-async.test.tsx b/src/__tests__/render-hook-async.test.tsx index f7acc6b1..0559db75 100644 --- a/src/__tests__/render-hook-async.test.tsx +++ b/src/__tests__/render-hook-async.test.tsx @@ -51,7 +51,7 @@ test('renderHookAsync supports legacy rendering option', async () => { expect(result.current).toEqual(42); }); -test('rerender function updates hook asynchronously', async () => { +test('rerenderAsync function updates hook asynchronously', async () => { function useTestHook(props: { value: number }) { const [state, setState] = React.useState(props.value); @@ -62,10 +62,10 @@ test('rerender function updates hook asynchronously', async () => { return state; } - const { result, rerender } = await renderHookAsync(useTestHook, { initialProps: { value: 5 } }); + const { result, rerenderAsync } = await renderHookAsync(useTestHook, { initialProps: { value: 5 } }); expect(result.current).toEqual(10); - await rerender({ value: 10 }); + await rerenderAsync({ value: 10 }); expect(result.current).toEqual(20); }); @@ -82,10 +82,10 @@ test('unmount function unmounts hook asynchronously', async () => { return 'test'; } - const { unmount } = await renderHookAsync(useTestHook); + const { unmountAsync } = await renderHookAsync(useTestHook); expect(cleanupCalled).toBe(false); - await unmount(); + await unmountAsync(); expect(cleanupCalled).toBe(true); }); @@ -214,29 +214,20 @@ test('handles custom hooks with complex logic', async () => { return { count, increment, decrement, reset }; } - const { result, rerender } = await renderHookAsync(useCounter, { initialProps: 5 }); - + const { result, rerenderAsync } = await renderHookAsync(useCounter, { initialProps: 5 }); expect(result.current.count).toBe(5); - // Test increment result.current.increment(); - await rerender(5); + await rerenderAsync(5); expect(result.current.count).toBe(6); - // Test decrement - result.current.decrement(); - await rerender(5); - expect(result.current.count).toBe(5); - - // Test reset - result.current.increment(); - result.current.increment(); - await rerender(5); - expect(result.current.count).toBe(7); - result.current.reset(); - await rerender(5); + await rerenderAsync(5); expect(result.current.count).toBe(5); + + result.current.decrement(); + await rerenderAsync(5); + expect(result.current.count).toBe(4); }); test('handles hook with cleanup and re-initialization', async () => { @@ -250,15 +241,13 @@ test('handles hook with cleanup and re-initialization', async () => { effectCount++; setValue(`${props.key}-effect`); - return () => { - cleanupCount++; - }; + return () => { cleanupCount++; }; }, [props.key]); return value; } - const { result, rerender, unmount } = await renderHookAsync(useTestHook, { + const { result, rerenderAsync, unmountAsync } = await renderHookAsync(useTestHook, { initialProps: { key: 'initial' }, }); @@ -266,12 +255,12 @@ test('handles hook with cleanup and re-initialization', async () => { expect(effectCount).toBe(1); expect(cleanupCount).toBe(0); - await rerender({ key: 'updated' }); + await rerenderAsync({ key: 'updated' }); expect(result.current).toBe('updated-effect'); expect(effectCount).toBe(2); expect(cleanupCount).toBe(1); - await unmount(); + await unmountAsync(); expect(effectCount).toBe(2); expect(cleanupCount).toBe(2); }); diff --git a/src/render-hook.tsx b/src/render-hook.tsx index 1720071f..2da52c60 100644 --- a/src/render-hook.tsx +++ b/src/render-hook.tsx @@ -11,8 +11,8 @@ export type RenderHookResult = { export type RenderHookAsyncResult = { result: React.RefObject; - rerender: (props: Props) => Promise; - unmount: () => Promise; + rerenderAsync: (props: Props) => Promise; + unmountAsync: () => Promise; }; export type RenderHookOptions = { @@ -90,7 +90,7 @@ export async function renderHookAsync( return { // Result should already be set after the first render effects are run. result: result as React.RefObject, - rerender: (hookProps: Props) => rerenderComponentAsync(), - unmount: unmountAsync, + rerenderAsync: (hookProps: Props) => rerenderComponentAsync(), + unmountAsync, }; } From b761b02f83ec442d101a5b22fa659e45f0e39428 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Fri, 8 Aug 2025 16:14:10 +0200 Subject: [PATCH 5/6] fix lint --- src/__tests__/render-hook-async.test.tsx | 8 ++++++-- src/render-hook.tsx | 3 ++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/__tests__/render-hook-async.test.tsx b/src/__tests__/render-hook-async.test.tsx index 0559db75..82ee858a 100644 --- a/src/__tests__/render-hook-async.test.tsx +++ b/src/__tests__/render-hook-async.test.tsx @@ -62,7 +62,9 @@ test('rerenderAsync function updates hook asynchronously', async () => { return state; } - const { result, rerenderAsync } = await renderHookAsync(useTestHook, { initialProps: { value: 5 } }); + const { result, rerenderAsync } = await renderHookAsync(useTestHook, { + initialProps: { value: 5 }, + }); expect(result.current).toEqual(10); await rerenderAsync({ value: 10 }); @@ -241,7 +243,9 @@ test('handles hook with cleanup and re-initialization', async () => { effectCount++; setValue(`${props.key}-effect`); - return () => { cleanupCount++; }; + return () => { + cleanupCount++; + }; }, [props.key]); return value; diff --git a/src/render-hook.tsx b/src/render-hook.tsx index 2da52c60..63f59c92 100644 --- a/src/render-hook.tsx +++ b/src/render-hook.tsx @@ -90,7 +90,8 @@ export async function renderHookAsync( return { // Result should already be set after the first render effects are run. result: result as React.RefObject, - rerenderAsync: (hookProps: Props) => rerenderComponentAsync(), + rerenderAsync: (hookProps: Props) => + rerenderComponentAsync(), unmountAsync, }; } From dd23563326ba133a726fd2d79c5e1d36fb8b6b76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Fri, 8 Aug 2025 16:38:51 +0200 Subject: [PATCH 6/6] . --- src/pure.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pure.ts b/src/pure.ts index 4d8d83a6..6b848308 100644 --- a/src/pure.ts +++ b/src/pure.ts @@ -21,6 +21,6 @@ export type { DebugFunction, } from './render'; export type { RenderAsyncOptions, RenderAsyncResult } from './render-async'; -export type { RenderHookOptions, RenderHookResult } from './render-hook'; +export type { RenderHookOptions, RenderHookResult, RenderHookAsyncResult } from './render-hook'; export type { Config } from './config'; export type { UserEventConfig } from './user-event';