Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
95b5046
fix(use-enhanced-reducer): Hold dispatches until the hook commits
cursoragent Sep 24, 2026
106a379
fix(core): Re-resolve fetches that settled into an uncommitted store
cursoragent Sep 24, 2026
8f0f5a9
test(react): Wait on conditions instead of fixed sleeps in pre-commit…
cursoragent Sep 24, 2026
255afaa
test(react): Run pre-commit race suite on React 17 and 18
cursoragent Sep 24, 2026
6e0e572
test(react): Reuse makeGate in pre-commit race suite
cursoragent Sep 24, 2026
b34f277
test(react): Pin fresh-manager endpoint calls to the root type in pre…
cursoragent Sep 24, 2026
4c130bb
test(use-enhanced-reducer): Cover the pre-commit dispatch hold on leg…
cursoragent Sep 24, 2026
8eb5c6b
revert(use-enhanced-reducer): Drop the pre-commit dispatch hold
cursoragent Oct 5, 2026
8aad167
fix(core): Hold fetch results until init() while DataProvider mounts
cursoragent Oct 5, 2026
e02b826
test(react): Cover DataProvider inside a lazy layout in the pre-commi…
cursoragent Oct 5, 2026
d99f11f
internal(core): Clear awaitingInit and parked results by assignment
cursoragent Oct 5, 2026
6b17ca6
test(core): Drive the awaitingInit tests through initManager
cursoragent Oct 5, 2026
071b573
Merge branch 'master' into cursor/precommit-dispatch-hold-6fdf
cursoragent Oct 5, 2026
51e9722
internal(core): Park fetches with the existing throttle flag
cursoragent Oct 5, 2026
c543adf
test(core): Let middleware failures fail the awaitingInit tests
cursoragent Oct 5, 2026
f1f1e8c
fix(core): Keep parked fetch results through cleanup() until the next…
cursoragent Oct 5, 2026
1ea7c28
internal(website): Update playground editor types for awaitingInit an…
cursoragent Oct 5, 2026
8032f45
test(core): Publish once across a same-turn init, cleanup, init
cursoragent Oct 5, 2026
ae95f8d
test(react): Cover StrictMode and an initially hidden Activity in the…
cursoragent Oct 5, 2026
0253d55
fix(core): Schedule the parked publish only when something is parked,…
cursoragent Oct 6, 2026
5d75d28
Merge remote-tracking branch 'origin/master' into cursor/precommit-di…
cursoragent Oct 6, 2026
47017e5
internal(website): Update playground editor types for publishParked a…
cursoragent Oct 6, 2026
90e73f5
test(react): Reuse ErrorBoundary and the console recorder in the prec…
cursoragent Oct 6, 2026
b4caf55
internal: Limit precommit changeset to core and react
claude Oct 21, 2026
74600bd
internal(core): Simplify parked publish in NetworkManager
claude Oct 21, 2026
c15fc64
experiment(react): Move pre-commit protection into an opt-in CommitGa…
claude Oct 21, 2026
88bb9c3
docs: Add CommitGateManager
claude Oct 22, 2026
06fd197
fix(react): Drop held results on any RESET and reject failed replays …
claude Oct 22, 2026
25b916a
fix(react): Keep replaying held results when one replay throws
claude Oct 22, 2026
e54b75f
Merge remote-tracking branch 'origin/master' into cursor/precommit-di…
claude Oct 22, 2026
d731107
internal(react): Use top-level type imports in the precommit race suite
claude Oct 22, 2026
c722d0a
fix(react): Add CommitGateManager in DataProvider instead of native g…
claude Oct 22, 2026
502f486
Simplify CommitGateManager replay and race suite
claude Oct 22, 2026
cac54f4
Regenerate playground editor types
claude Oct 22, 2026
1df2fdf
docs(blog): Add CommitGateManager to v0.19 release post
claude Oct 22, 2026
596d6ea
docs(blog): Frame CommitGateManager entry as a React Native fix
claude Oct 22, 2026
9b82260
fix(react): Show a replay that throws as that fetch's error, and shar…
claude Oct 22, 2026
466b0a7
test(react): Cover a held error result that throws on replay
claude Oct 22, 2026
28c1b2b
fix(react): Never let a replay throw escape, and key the native gate …
claude Oct 22, 2026
7c0767e
docs: Frame CommitGateManager around Expo Router's lazy layouts, not …
claude Oct 22, 2026
c4df3ca
Merge master
claude Oct 24, 2026
953aabc
fix(react): CommitGateManager drops pre-reset results and stays open …
claude Oct 24, 2026
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
2 changes: 2 additions & 0 deletions .agents/skills/data-client-react/references/DataProvider.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions .agents/skills/data-client-setup/references/DataProvider.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions .changeset/precommit-fetch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
'@data-client/react': patch
---

Add `CommitGateManager` for DataProvider inside a lazy layout, such as Expo Router's root layout

[useSuspense](/docs/api/useSuspense) and `use(useFetch())` could stay on the fallback, or refetch in a loop, when their fetch finished before [DataProvider](/docs/api/DataProvider) had mounted. Expo Router wraps routes, including the root `_layout.tsx`, in `React.lazy` when async routes are on (the default on web), so React can throw away the provider's first render. Any DataProvider inside `React.lazy`, or next to something that suspends outside it, is affected. This is not specific to React Native.

Add `CommitGateManager` before the other managers. It holds those results until the provider mounts:

```tsx
<DataProvider managers={[new CommitGateManager(), ...getDefaultManagers()]}>
```

On React Native, DataProvider adds it automatically.
70 changes: 70 additions & 0 deletions docs/core/api/CommitGateManager.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
---
title: CommitGateManager - Keep fetches that resolve before DataProvider mounts
sidebar_label: CommitGateManager
frameworks: [react]
---

import ProviderManagers from '../shared/_provider_managers.mdx';

# CommitGateManager

Holds fetch results that arrive before [&lt;DataProvider /&gt;](./DataProvider.md) mounts, then
applies them once it does.

React can render a `DataProvider` and then throw that render away. This happens when the provider
sits inside a lazy-loaded layout, or when something outside the provider suspends during its first
render. A fetch that resolves in between has no store to land in, so [useSuspense()](./useSuspense.md)
would stay on its fallback. `CommitGateManager` keeps those results and replays them into the store
that mounts, so the endpoint is called only once.

:::info implements

`CommitGateManager` implements [Manager](./Manager.md)

:::

## Usage

Add it first when `DataProvider` renders inside a [lazy](https://react.dev/reference/react/lazy)
layout or route, or next to a component that can suspend outside it. The most common case is
[Expo Router](https://docs.expo.dev/router/introduction/): with async routes on, which is the default
on web, it wraps the root `_layout.tsx` in `React.lazy`. This isn't specific to React Native.

On React Native, [DataProvider](./DataProvider.md#managers) adds it first to its `managers` for you.

<ProviderManagers imports={['CommitGateManager', 'getDefaultManagers']}>

```ts
// highlight-next-line
const managers = [new CommitGateManager(), ...getDefaultManagers()];
```

</ProviderManagers>

:::note

Put it first, in the same `managers` list as the [NetworkManager](./NetworkManager.md) it protects,
and give each `DataProvider` its own `CommitGateManager`.

:::

:::tip

Placing `DataProvider` at the root, with every [Suspense](https://react.dev/reference/react/Suspense)
boundary inside it, avoids the race without this manager.

:::

## Members

### middleware

#### Processed Actions

- [setResponse](./Controller.md#setResponse) is held until the provider mounts.
- [resetEntireStore](./Controller.md#resetEntireStore) drops held results, and results fetched before it that settle later.
- During server rendering, nothing is held, since a server render never mounts.

#### Dispatched Actions

- [setResponse](./Controller.md#setResponse) replays each held result into the mounted store.
2 changes: 2 additions & 0 deletions docs/core/api/DataProvider.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ List of [Manager](./Manager.md)s use. This is the main extensibility point of th

[getDefaultManagers()](./getDefaultManagers.md) can be used to extend the default managers.

On React Native, [CommitGateManager](./CommitGateManager.md) is added first unless `managers` already includes it.

Default Production:

```typescript
Expand Down
6 changes: 4 additions & 2 deletions packages/react/src/components/DataProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type { JSX } from 'react';
import DataStore from './DataStore.js';
import type { DevToolsPosition } from './DevToolsButton.js';
import { getDefaultManagers } from './getDefaultManagers.js';
import platformManagers from './platformManagers.js';
import { SSR } from './LegacyReact.js';
import { renderDevButton } from './renderDevButton.js';
import { ControllerContext } from '../context.js';
Expand Down Expand Up @@ -50,8 +51,9 @@ export default function DataProvider({
controllerRef.current = new Controller({ gcPolicy: gcRef.current });
//TODO: bind all methods so destructuring works

const managersRef: React.RefObject<Manager[]> = useRef<any>(managers);
if (!managersRef.current) managersRef.current = getDefaultManagers();
const managersRef: React.RefObject<Manager[]> = useRef<any>(undefined);
if (!managersRef.current)
managersRef.current = platformManagers(managers ?? getDefaultManagers());

// run in a useEffect in DataStore
// eslint-disable-next-line react-hooks/exhaustive-deps
Expand Down
28 changes: 28 additions & 0 deletions packages/react/src/components/__tests__/precommit-race.native.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { ReactNode } from 'react';
import { Text } from 'react-native';
import TestRenderer from 'react-test-renderer';

import { registerPrecommitRaceTests } from './precommit-race.node-suite';
import type { RaceHost } from './precommit-race.node-suite';

const host: RaceHost = {
addsGate: true,
Text: ({ children }: { children?: ReactNode }) => <Text>{children}</Text>,
createRenderer() {
let renderer: TestRenderer.ReactTestRenderer | undefined;
return {
render(node) {
if (!renderer) renderer = TestRenderer.create(node);
else renderer.update(node);
},
read() {
return JSON.stringify(renderer?.toJSON() ?? '');
},
unmount() {
renderer?.unmount();
},
};
},
};

registerPrecommitRaceTests(host);
Loading
Loading