useSyncExternalStore: a subscriber under a suspended boundary renders with rewound props and the new snapshot (0.2.4)
Mantenedores costumam responder em até 1 dia
Ninguém assumiu esta issue ainda.
Avaliação
- Dificuldade
- 4/5
- Tempo estimado
- 3-5 dias
- Facilidade para iniciantes
- 18/100
- Tipo de issue
- Bug
- Clareza
- Razoavelmente clara
- Status de atividade
- Ativa
- Stack de tecnologia
- react, typescript, vite
- Domínio
- frontend
Direção de pesquisa
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.
Escrita pelo modelo de indexação a partir do texto da issue.
Descrição
🤖 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.
- Linguagem predominante
- TypeScript
- Estrelas
- 537
- Forks
- 22
- Merge médio
- 2h 19min
- PRs com merge (30d)
- 24
Preparar o ambiente
Este projeto não oferece contêiner de desenvolvimento, Dockerfile nem guia de contribuição, então a configuração fica por sua conta: comece pelo README e veja nosso guia da primeira contribuição para os passos gerais.
Primeiros passos
- Leia a issue inteira e depois o guia de contribuição do projeto.
- Comente na issue dizendo que vai assumir — evita que duas pessoas façam o mesmo trabalho.
- Faça um fork do repositório e trabalhe em uma branch.
- Abra um pull request que referencie o número da issue.
Mais de TanStack/redact
-
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 85/100
Mantenedores costumam responder em até 1 dia
-
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 78/100
Mantenedores costumam responder em até 1 dia
-
Dificuldade 3/5 1-2 dias Facilidade para iniciantes 42/100
Mantenedores costumam responder em até 1 dia
-
Dificuldade 5/5 Mais de uma semana Facilidade para iniciantes 18/100
Mantenedores costumam responder em até 1 dia
-
Dificuldade 4/5 3-5 dias Facilidade para iniciantes 18/100
Mantenedores costumam responder em até 1 dia
Todas as issues de TanStack/redact
Issues semelhantes
-
Remove unused dependenciesTalvez já em andamento @alastair-smith assumiu hoje. Abertachore other
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 78/100
alastair-smith/card-games#384 ·
Mantenedores costumam responder em até 1 dia
-
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 62/100
code-yeongyu/senpi#5571 ·
Mantenedores costumam responder em até 1 dia
-
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 78/100
nexu-io/open-design#9262 · 1 comentário ·
Mantenedores costumam responder em até 1 dia
-
friction
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 72/100
kentcdodds/kody#4753 ·
-
community first-timers-only good first issue hacktoberfest help wanted low hanging fruit up-for-grabs
Dificuldade 1/5 Menos de uma hora Facilidade para iniciantes 62/100
lingdojo/kana-dojo#33508 · 1 comentário · 5 reações ·
Mantenedores costumam responder em até 1 dia