Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions .changeset/render-data-hook-suspend-effects.md
Original file line number Diff line number Diff line change
@@ -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.
20 changes: 19 additions & 1 deletion packages/test/src/makeRenderDataClient/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -23,6 +23,17 @@ import { MockProps } from '../mockTypes.js';

const activeCleanups = new Set<() => void>();

type KickRef = { current?: React.Dispatch<React.SetStateAction<number>> };

/** Sibling that commits even when the hook suspends, so we can schedule a real update. */
function Kick({ kickRef }: { kickRef: KickRef }) {
const [, setKick] = useState(0);
useLayoutEffect(() => {
kickRef.current = setKick;
}, [kickRef]);
return null;
}

if (typeof afterEach === 'function') {
afterEach(() => {
for (const fn of activeCleanups) fn();
Expand Down Expand Up @@ -138,19 +149,26 @@ export default function makeRenderDataHook(
}
: ProviderWithResolver;

const kickRef: KickRef = {};
const wrapper: React.ComponentType<any> = ({
children,
...props
}: React.PropsWithChildren<P>) => (
<ProviderWithWrapper {...(props as any)}>
<Suspense fallback={null}>{children}</Suspense>
<Kick kickRef={kickRef} />
</ProviderWithWrapper>
);

const ret: any = renderHook(callback, {
...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?.(count => count + 1);
});
ret.controller = nm['controller'];
ret.cleanup = cleanup;
ret.allSettled = allSettled;
Expand Down
Loading