From 74e232b85316a0711732b37c4a8493a2e3ea52a2 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 24 Sep 2026 16:40:30 +0000 Subject: [PATCH 1/2] fix(test): Run mount effects when renderDataHook suspends A suspending first render inside synchronous act() drops React's passive-effect task, so provider mount effects never run. Schedule one sibling setState after mount so React flushes those effects. Co-authored-by: Nathaniel Tucker --- .../render-data-hook-suspend-effects.md | 7 +++++++ .../test/src/makeRenderDataClient/index.tsx | 21 ++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 .changeset/render-data-hook-suspend-effects.md diff --git a/.changeset/render-data-hook-suspend-effects.md b/.changeset/render-data-hook-suspend-effects.md new file mode 100644 index 000000000000..51fa328c9bff --- /dev/null +++ b/.changeset/render-data-hook-suspend-effects.md @@ -0,0 +1,7 @@ +--- +'@data-client/test': patch +--- + +Fix mount effects when `renderDataHook()` suspends on the first render + +`renderDataHook()` and `makeRenderDataHook()` now run provider mount effects when the first render suspends, including `use(useFetch())`. The hook result stays unresolved until the data arrives. diff --git a/packages/test/src/makeRenderDataClient/index.tsx b/packages/test/src/makeRenderDataClient/index.tsx index 0bd587ab2be5..662817876328 100644 --- a/packages/test/src/makeRenderDataClient/index.tsx +++ b/packages/test/src/makeRenderDataClient/index.tsx @@ -8,7 +8,7 @@ import { GenericDispatch, DataClientDispatch, } from '@data-client/react'; -import React, { memo, Suspense } from 'react'; +import React, { memo, Suspense, useLayoutEffect, useState } from 'react'; import { renderHook, @@ -23,6 +23,17 @@ import { MockProps } from '../mockTypes.js'; const activeCleanups = new Set<() => void>(); +/** Sibling that commits even when the hook suspends, so we can schedule a real update. */ +function Kick({ kickRef }: { kickRef: { current: (() => void) | null } }) { + const [, setKick] = useState(0); + useLayoutEffect(() => { + kickRef.current = () => { + setKick(count => count + 1); + }; + }, [kickRef]); + return null; +} + if (typeof afterEach === 'function') { afterEach(() => { for (const fn of activeCleanups) fn(); @@ -138,12 +149,15 @@ export default function makeRenderDataHook( } : ProviderWithResolver; + // Captured in Kick's layout effect, which still runs inside the sync mount act. + const kickRef: { current: (() => void) | null } = { current: null }; const wrapper: React.ComponentType = ({ children, ...props }: React.PropsWithChildren

) => ( {children} + ); @@ -151,6 +165,11 @@ export default function makeRenderDataHook( ...options, wrapper, }); + // A suspending first render inside sync act() drops the passive-effect task. + // A real setState gives React a task that flushes those effects. An empty act() does not. + act(() => { + kickRef.current?.(); + }); ret.controller = nm['controller']; ret.cleanup = cleanup; ret.allSettled = allSettled; From 734291ddd2d5488949c3099d5138a1f62e723e5f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 24 Sep 2026 16:52:35 +0000 Subject: [PATCH 2/2] refactor(test): Store Kick's setState directly in renderDataHook Co-authored-by: Nathaniel Tucker --- packages/test/src/makeRenderDataClient/index.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/test/src/makeRenderDataClient/index.tsx b/packages/test/src/makeRenderDataClient/index.tsx index 662817876328..ef572efaf238 100644 --- a/packages/test/src/makeRenderDataClient/index.tsx +++ b/packages/test/src/makeRenderDataClient/index.tsx @@ -23,13 +23,13 @@ import { MockProps } from '../mockTypes.js'; const activeCleanups = new Set<() => void>(); +type KickRef = { current?: React.Dispatch> }; + /** Sibling that commits even when the hook suspends, so we can schedule a real update. */ -function Kick({ kickRef }: { kickRef: { current: (() => void) | null } }) { +function Kick({ kickRef }: { kickRef: KickRef }) { const [, setKick] = useState(0); useLayoutEffect(() => { - kickRef.current = () => { - setKick(count => count + 1); - }; + kickRef.current = setKick; }, [kickRef]); return null; } @@ -149,8 +149,7 @@ export default function makeRenderDataHook( } : ProviderWithResolver; - // Captured in Kick's layout effect, which still runs inside the sync mount act. - const kickRef: { current: (() => void) | null } = { current: null }; + const kickRef: KickRef = {}; const wrapper: React.ComponentType = ({ children, ...props @@ -168,7 +167,7 @@ export default function makeRenderDataHook( // A suspending first render inside sync act() drops the passive-effect task. // A real setState gives React a task that flushes those effects. An empty act() does not. act(() => { - kickRef.current?.(); + kickRef.current?.(count => count + 1); }); ret.controller = nm['controller']; ret.cleanup = cleanup;