/** * A/B concurrency tests for render-phase updates (setState during render) * in renders React discards and replays. * * A render-phase dispatch lives only inside its render attempt: tap drains it * within renderResourceFiber, and a discarded attempt is rolled back * (setRootVersion resets workInProgress and clears cell queues). The dispatch * therefore survives a discard only if the retry re-derives it from the * render's inputs. These tests run the same scenario in two worlds: * * react — the hooks run directly in the component (React's render-phase * queue semantics are the oracle) * tap — the hooks run in a useTapRoot sub-root read via * useSyncExternalStore * * and assert the committed, observable output matches. Render-attempt * interleavings are deliberately NOT compared; committed state must not * differ. */ /* oxlint-disable react/rules-of-hooks -- the world branch is fixed per test run */ import { describe, it, expect, afterEach } from "vitest"; import { StrictMode, startTransition, Suspense, useMemo, useReducer, useRef, useState, useSyncExternalStore, } from "react"; import { render, screen, act, cleanup } from "@testing-library/react"; import { useTapRoot } from "../../index"; import { cleanupAllResources } from "../test-utils"; afterEach(() => { cleanupAllResources(); cleanup(); }); type WorldName = "react" | "tap"; const useInWorld = (world: WorldName, useBody: () => T): T => { if (world === "react") return useBody(); const root = useTapRoot(function Sub() { return useBody(); }); return useSyncExternalStore(root.subscribe, root.getValue, root.getValue); }; describe("render-phase updates under concurrent rendering (react vs tap)", () => { it("pure derivations re-derive across an interrupted transition", async () => { const run = async (world: WorldName): Promise => { let api!: { add: (x: number) => void }; const useDerived = () => { const [n, addN] = useReducer((s: number, x: number) => s + x, 0); const [doubled, setDoubled] = useState(0); // The classic adjust-during-render pattern: a pure function of `n`, // so any discarded attempt's dispatch is re-derived on retry. if (doubled !== n * 2) setDoubled(n * 2); return useMemo( () => ({ n, doubled, add: (x: number) => addN(x) }), [n, doubled], ); }; function App() { const value = useInWorld(world, useDerived); api = value; const [mode, setMode] = useState("idle"); return ( <>