Skip to content

test(react): Lock concurrent rendering GOALS on public APIs - #4094

Draft
ntucker wants to merge 9 commits into
masterfrom
cursor/concurrent-rendering-goal-tests-bbf3
Draft

ntucker wants to merge 9 commits into
masterfrom
cursor/concurrent-rendering-goal-tests-bbf3

Conversation

@ntucker

@ntucker ntucker commented Sep 18, 2026

Copy link
Copy Markdown
Collaborator

Fixes # .

Motivation

Lock the GOALS bullet Fully concurrent rendering on the latest React with tests against public @data-client/react (DataProvider, useSuspense, useCache, useLive, useController) plus React itself. No packages/* source changes and no changeset.

This is a new PR off master. It does not push to #4090, does not copy #4092 waiter / RSC-first contracts, and does not assert a universal “all live Context publishes preserve pending siblings.”

Existing probes establish two conditional cases, not one universal rule:

  • Master’s stable, gate-before-consumer shape (publish through memoized DataStore while a sibling whose data consumer has not rendered past an outer gate is <!--$?-->) remains dehydrated on 19.2.3 / 19.3.0 and later hydrates in place.
  • PR 4090’s two-pending live-publish shape client-renders the sibling on 19.2.3 and 19.3.0; startTransition did not rescue that composition.

Shape 1 is a green regression guard for the narrower gate-before-consumer statement on 19.x. Shapes 2 and 3 are causal controls whose outcomes are recorded per React version.

Solution

  • __tests__/reactCommitProbe.ts + __tests__/streamingHarness.tsx: DevTools commit-priority probe (self-calibrated) and two-phase renderToPipeableStream / hydrateRoot replay. The Node server build is required so jsdom’s browser export on React 18 still exposes renderToPipeableStream; executeScripts hoists 18’s $RC function declarations onto the global.
  • concurrent-probe-controls.web.tsx: uSES is Immediate even inside startTransition; plain setState is Normal; discrete setResponse is Immediate; recreating Suspense client-renders a pending boundary. On React 18 that control logs “This Suspense boundary received an update before it finished hydrating… wrap the original update in startTransition.” The recreate test expects that warning and drains it so the suite afterEach empty-console assert stays strict. Hydration and transition suites do not filter it. recordConsoleErrors is unchanged.
  • concurrent-transition.web.tsx: startTransition over useSuspense keeps the committed UI, accepts urgent input, then commits B; isolated store publishes are non-Immediate; zero flushSync / useSyncExternalStore.
  • concurrent-hydration.web.tsx: shape 1 green lock on 19.x; shapes 2–3 recorded per version.
  • useCacheState.web.tsx: hoisted throwing useSyncExternalStore; React 19 use(StateContext); React 18 useContext labeled fallback.
  • CircleCI unit_tests-19.3: delete root React resolutions, yarn up react@19.3.0, run the focused ReactDOM suites. The latest row stays on the 19.2.3 pin.

Simplify (same PR, no assertion or CI-row changes): shared replayRest / getHydrateRoot / probe calibration helpers; hydration streamPage + publishAOnTransition; collapsed identical shape 2/3 table rows.

Per-version results

Focused suites: packages/react/src/__tests__/concurrent-.*\\.web\\.tsx + useCacheState.web.tsx. 24 passed, 1 skipped on every version below. No it.failing.

React Where Result
16 / 17 CI unit_tests-^17 Suites describe.skip (no react-dom/client / useTransition). useCacheState 16/17 isolate still runs.
18.3.1 CI unit_tests-^18 (local yarn-up) Recreate-Suspense client-renders pending B and logs the hydration-update warning (expected on 18 only). After expecting/draining that warning (2c592e7d30): probe-controls 4 passed (3.621 s); all concurrent suites 20 passed; useCacheState 4 passed, 1 skipped. Shape 1 transition publish: Normal commit, B stays pending, host title stays todo A. Shape 1 default-lane: title becomes v2, B is client-rendered.
19.2.3 repo pin; CI latest 4 suites, 24 passed, 1 skipped. Shape 1 green (identity, interactivity, in-place reveal). Probe-controls re-ran after the 18 warning allowance: 4 passed, 2.307 s.
19.3.0 new CI unit_tests-19.3 (local yarn-up) 4 suites, 24 passed, 1 skipped, 3.705 s. Same shape 1 green lock as 19.2.3.

The adapter-level flushSync assertion needs #4090 code and is out of scope.

Shape 2 / shape 3 recorded table (public DataProvider)

Version Shape pending after publish B rendered after publish streamed node adopted fetchIds
18.3.1 2 read-then-suspend B stays pending no yes []
18.3.1 3 two-pending A and B stay pending no yes []
19.2.3 2 read-then-suspend B stays pending no yes []
19.2.3 3 two-pending A and B stay pending no yes []
19.3.0 2 read-then-suspend B stays pending no yes []
19.3.0 3 two-pending A and B stay pending no yes []

Composition difference vs #4090: on public master, the two-pending live-publish topology (shape 3) does not client-render B on 18.3.1, 19.2.3, or 19.3.0 (fetchIds: [], streamed node adopted). PR 4090’s adapter-shaped live HYDRATE did client-render the sibling (clientFetches: ["B"]). That difference is reported, not rewritten.

Open questions

None for this PR. Doc patch intents for layering/integrity/19.3 spike docs belong to other subagents.

Open in Web Open in Cursor 

cursoragent and others added 6 commits September 18, 2026 03:49
Install a DevTools hook before react-dom/client so suites can record
scheduler priority, and share two-phase renderToPipeableStream replay
helpers for hydrateRoot tests.

Co-authored-by: Nathaniel Tucker <me@ntucker.me>
Calibrate Immediate vs Normal commits, document discrete-event SyncLane,
and show that recreating Suspense elements client-renders a pending
boundary so streamed nodes are not adopted.

Co-authored-by: Nathaniel Tucker <me@ntucker.me>
Keep the committed UI, accept urgent input, and require non-Immediate
store publishes for transition/default setResponse without flushSync or
useSyncExternalStore.

Co-authored-by: Nathaniel Tucker <me@ntucker.me>
Lock gate-before-consumer identity and in-place reveal, and record
per-version outcomes for read-then-suspend and two-pending public
DataProvider topologies.

Co-authored-by: Nathaniel Tucker <me@ntucker.me>
Hoist a throwing useSyncExternalStore wrapper so public hooks cannot
capture the real API, and label the React 19 use() vs React 18
useContext fallback.

Co-authored-by: Nathaniel Tucker <me@ntucker.me>
Delete root React resolutions, install 19.3.0, and run the concurrent
rendering suites so latest React is locked in CI rather than the 19.2.3 pin.

Co-authored-by: Nathaniel Tucker <me@ntucker.me>
@changeset-bot

changeset-bot Bot commented Sep 18, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 2c592e7

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel

vercel Bot commented Sep 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
docs-site Ignored Ignored Preview Sep 18, 2026 4:17am UTC

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

Size Change: 0 B

Total Size: 81 kB

ℹ️ View Unchanged
Filename Size
examples/test-bundlesize/dist/App.js 1.46 kB
examples/test-bundlesize/dist/polyfill.js 307 B
examples/test-bundlesize/dist/rdcClient.js 10.9 kB
examples/test-bundlesize/dist/rdcEndpoint.js 8.07 kB
examples/test-bundlesize/dist/react.js 59.6 kB
examples/test-bundlesize/dist/webpack-runtime.js 784 B

compressed-size-action

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benchmark React

Details
Benchmark suite Current: 2c592e7 Previous: 3fa47c8 Ratio
data-client: getlist-100 126.58 ops/s (± 4.5%) 137.94 ops/s (± 4.5%) 1.09
data-client: getlist-500 41.07 ops/s (± 6.0%) 44.05 ops/s (± 3.8%) 1.07
data-client: update-entity 322.58 ops/s (± 7.6%) 384.62 ops/s (± 9.3%) 1.19
data-client: update-user 312.5 ops/s (± 6.9%) 333.33 ops/s (± 8.6%) 1.07
data-client: getlist-500-sorted 43.01 ops/s (± 10.3%) 43.11 ops/s (± 9.4%) 1.00
data-client: update-entity-sorted 285.71 ops/s (± 8.8%) 312.5 ops/s (± 7.9%) 1.09
data-client: update-entity-multi-view 285.71 ops/s (± 6.7%) 327.96 ops/s (± 7.5%) 1.15
data-client: list-detail-switch-10 7.16 ops/s (± 6.4%) 9.9 ops/s (± 9.4%) 1.38
data-client: update-user-10000 72.46 ops/s (± 5.6%) 72.2 ops/s (± 14.8%) 1.00
data-client: invalidate-and-resolve 34.01 ops/s (± 5.4%) 37.67 ops/s (± 5.6%) 1.11
data-client: unshift-item 204.08 ops/s (± 5.5%) 219.81 ops/s (± 6.0%) 1.08
data-client: delete-item 270.27 ops/s (± 4.7%) 285.71 ops/s (± 3.8%) 1.06
data-client: move-item 161.29 ops/s (± 8.4%) 177.01 ops/s (± 7.9%) 1.10

This comment was automatically generated by workflow using github-action-benchmark.

Load renderToPipeableStream from the Node server build, hoist React 18
$RC function declarations onto the global, and lock 18.3/19.2/19.3
shape 2–3 rows. React 18 default-lane publishes still client-render a
pending sibling; transition publishes keep it dehydrated without host text.

Co-authored-by: Nathaniel Tucker <me@ntucker.me>
@codecov

codecov Bot commented Sep 18, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.84%. Comparing base (44cb7e6) to head (2c592e7).

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #4094   +/-   ##
=======================================
  Coverage   97.84%   97.84%           
=======================================
  Files         156      156           
  Lines        3057     3057           
  Branches      612      612           
=======================================
  Hits         2991     2991           
  Misses         18       18           
  Partials       48       48           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Share replayRest, getHydrateRoot, and probe calibration helpers so the
suites stop copying stream install and notify-store boilerplate. Keep
reactCommitProbe as the first import so the DevTools hook still installs
before react-dom/client.

Co-authored-by: Nathaniel Tucker <me@ntucker.me>
@ntucker

ntucker commented Sep 18, 2026

Copy link
Copy Markdown
Collaborator Author

FOLLOW_UP (Staff Architect) — does not block merge after Ready / /simplify.

Solid GOALS lock: public @data-client/react only, no packages/* source, honest conditional cases (shape 1 green vs shapes 2–3 recorded). Harnesses + self-calibrated DevTools probe are the right architecture; 19.3 CI row is correctly focused.

After land, please consider:

  1. Drop duplicate calibrationconcurrent-hydration.web.tsx and concurrent-transition.web.tsx still copy the uSES-Immediate / plain-setState-Normal blocks that concurrent-probe-controls.web.tsx already owns post-0acb4b2. Keep one suite as the probe control; let the others rely on it (or a shared beforeAll helper).

  2. Soften B:0 / B:1 coupling — Shape 1’s green path pins React streaming Suspense template ids. If a future React minor renumbers boundaries, prefer asserting dehydrated pending via <!--$?--> + “B not in renders / no fetch” where that still proves the contract.

  3. Retire unit_tests-19.3 when latest pins ≥19.3 — Avoid a permanent extra matrix job once the repo pin catches up.

  4. DevTools priority maintenance — Treat Immediate/Normal remaps as probe recalibration, not a data-client product break (already self-calibrated; call that out in the spike/docs follow-ups).

No CHANGE_THIS_PR. Draft is fine to review; merge gate remains Ready after /simplify + green checks.

The recreate-Suspense probe client-renders the pending sibling on 18, which
logs the pre-hydration update warning. Expect that warning in this case only;
leave other suites' empty-console afterEach asserts unchanged.

Co-authored-by: Nathaniel Tucker <me@ntucker.me>
@ntucker

ntucker commented Sep 18, 2026

Copy link
Copy Markdown
Collaborator Author

FOLLOW_UP (no change required on this PR)

Direction looks right for a public-API concurrent-rendering goal lock: shape 1 as the green regression guard, shapes 2/3 as per-minor recorded controls (not #4090 adapter contracts), shared probe + streaming harness after simplify, and a focused unit_tests-19.3 row that deletes resolutions rather than fighting the 19.2.3 pin.

When you next touch this area (not blocking Ready/merge):

  1. Soften B:0 / B:1 — prefer discovering pending template[id] from the installed shell (or fallback-text + <!--$?-->) so a React HTML-protocol id tweak doesn’t brick the goal suite.
  2. DevTools hook — consider installing __REACT_DEVTOOLS_GLOBAL_HOOK__ from ReactDOM scripts/testSetup.js so suites can drop the import/order ritual and stay safe if a jest.mock('react-dom') ever requireActuals client early. Keep per-suite calibration fail-loud either way.
  3. Fixture drift — shared Todo/Endpoint/mockInitialState helper; keep Immediate/Normal calibration primarily in concurrent-probe-controls unless a file’s mocks need a local smoke.
  4. 19.3 → pin — when 19.3 becomes latest, widen the CI row past the concurrent path glob and add shape 2/3 table rows for each new React minor.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants