useSyncExternalStore: a subscriber under a suspended boundary renders with rewound props and the new snapshot (0.2.4)
I maintainer di solito rispondono entro 1 giorno
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 4/5
- Tempo stimato
- 3-5 giorni
- Idoneità per principianti
- 18/100
- Tipo di issue
- Bug
- Chiarezza
- Abbastanza chiara
- Stato di attività
- Attiva
- Stack tecnologico
- react, typescript, vite
- Ambito
- frontend
Direzione di ricerca
Start in packages/redact/src/dom/reconcile.ts, in renderUpdates (around lines 337-371), where pending fibers are sorted and a RenderSuspenseCapture rewinds the commit. Run the reproduction in the issue, with and without ?control, to confirm the Item render sees id: "a" after items.a is cleared. Done when the repro shows B loaded instead of the TypeError, and Redact's own test suite passes with the change.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
🤖 Issue drafted with Claude Code. Reproduced and verified before posting.
Version: @tanstack/redact 0.2.4 (also 0.2.3). Baseline: react/react-dom 19.3.1. Chromium, production build with vite 8.3.5, @vitejs/plugin-react 6.2.1 and the redact() plugin.
A store update switches a component to a new prop, and the new render suspends. Redact then renders the same component with its old props and the new store snapshot. In the repro below, that render reads a store that the update has just emptied, and the app crashes. React shows the new content once the data resolves.
This looks like a variant of #87, which was fixed in 0.2.2: here the child keeps its position and type and only its prop changes.
Reproduction
Vite app with react() + redact(), then call go() from the console:
import { Suspense, use, useSyncExternalStore } from "react"
import { createRoot } from "react-dom/client"
const store = (value) => {
const listeners = new Set()
return {
get: () => value,
set: (next) => { value = next },
emit: () => listeners.forEach((l) => l()),
subscribe: (l) => (listeners.add(l), () => listeners.delete(l)),
}
}
const page = store("a")
const items = { a: store({ label: "A" }), b: store({ label: "B" }) }
let slow
const load = () => (slow ??= new Promise((resolve) => setTimeout(() => resolve("loaded"), 300)))
const suspend = !new URLSearchParams(location.search).has("control")
function Item({ id }) {
const item = useSyncExternalStore(items[id].subscribe, items[id].get)
return <p>{item.label} {id === "b" && suspend ? use(load()) : null}</p>
}
function Page() {
const id = useSyncExternalStore(page.subscribe, page.get)
return <Item id={id} />
}
createRoot(document.getElementById("root")).render(<Suspense fallback="loading"><Page /></Suspense>)
// Switch to "b" and drop "a", then notify (like a router updating its stores).
window.go = () => { page.set("b"); items.a.set(undefined); page.emit(); items.a.emit() }
Result 2 s after go(), 10 runs each:
| React 19.3.1 | Redact 0.2.4 | |
|---|---|---|
| As written | 10/10 B loaded |
10/10 TypeError: Cannot read properties of undefined (reading 'label'), empty root |
?control (nothing suspends) |
10/10 B |
10/10 B |
The result is the same with the two notifications in the other order, and with the update wrapped in startTransition.
Real-world impact
With TanStack Router 1.170.46, this crashes client navigation from /account to /account/profile when the child route's component suspends during render: Cannot read properties of undefined (reading 'routeId') in MatchImpl, caught by the route's error boundary. I first hit it in a TanStack Start app on 0.2.3. A router-only repro (a layout route with an <Outlet />, and a child route whose component calls use(promise)) still crashes on 0.2.4. The same routes without the layout do not crash.
Likely cause
Tracing renderUpdates (reconcile.ts#L337-L371) shows this sequence:
- Both
PageandItemare pending and sorted by depth. Pagere-renders and its childItem(nowid: "b") suspends. TheRenderSuspenseCapturethrown out ofPage's render rewinds the commit, which restoresItem's props toid: "a", then the boundary is rescheduled.- The loop continues to
Item, which is still pending. It renders withid: "a"whileitems.ais alreadyundefined.
React never renders a component with props its parent no longer gives it: the boundary's retry renders the subtree top-down.
A local patch fixes both this repro and the router repro on 0.2.4. It skips pending fibers that sit under a boundary that suspended earlier in the same batch, and lets the boundary retry render them:
const suspended = new Set<Fiber>()
for (const fiber of pending) {
if (!fiber.g || fiber.u || fiber.pd)
continue
let ancestor = fiber.p
while (ancestor && !suspended.has(ancestor))
ancestor = ancestor.p
if (ancestor)
continue
// ...
if (error instanceof RenderSuspenseCapture) {
rewindCommit(checkpoint)
error.b.b.u = true
suspended.add(error.b)
scheduleUpdate(error.b)
continue
}
I have only checked it against these repros, not against Redact's test suite. A related case, where the boundary retry does not reach a pending subscriber behind an unchanged memo, is filed separately as #130.
- Lingua principale
- TypeScript
- Stelle
- 537
- Fork
- 22
- Merge medio
- 2h 19m
- PR unite (30g)
- 24
Preparare l'ambiente
Questo progetto non fornisce container di sviluppo, Dockerfile né guida per i contributori, quindi l'ambiente è a tuo carico: parti dal suo README e consulta la nostra guida al primo contributo per i passaggi generali.
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di TanStack/redact
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 85/100
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 3/5 1-2 giorni Idoneità per principianti 42/100
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 5/5 Più di una settimana Idoneità per principianti 18/100
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 4/5 3-5 giorni Idoneità per principianti 18/100
I maintainer di solito rispondono entro 1 giorno
Tutte le issue di TanStack/redact
Issue simili
-
🐛 bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
LibreChat-AI/LibreChat#17733 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 70/100
TypeStrong/typedoc#3150 ·
-
accessibility bug good first issue
Difficoltà 2/5 1-3 ore Idoneità per principianti 85/100
I maintainer di solito rispondono entro 1 giorno
-
Normalize image-source MIME types before SVG dispatchForse già presa @HarsH-1877 l’ha presa oggi. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 75/100
tscircuit/image-utils#58 · 1 commento ·
-
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 74/100
I maintainer di solito rispondono entro 2 giorni