useSyncExternalStore: a subscriber under a suspended boundary renders with rewound props and the new snapshot (0.2.4)
Maintainer thường phản hồi trong vòng 1 ngày
Đánh giá
- Độ khó
- 4/5
- Thời gian dự kiến
- 3-5 ngày
- Mức phù hợp với người mới
- 18/100
- Loại issue
- Lỗi
- Độ rõ ràng
- Khá rõ ràng
- Mức độ hoạt động
- Sôi nổi
- Công nghệ
- react, typescript, vite
- Lĩnh vực
- frontend
Hướng nghiên cứu
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.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
🤖 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.
- Ngôn ngữ chính
- TypeScript
- Star
- 537
- Fork
- 22
- Merge trung bình
- 11 giờ 37 phút
- Pull request đã merge (30 ngày)
- 35
Chuẩn bị môi trường
Dự án này không cung cấp dev container, Dockerfile hay hướng dẫn đóng góp, nên bạn cần tự thiết lập môi trường: hãy bắt đầu từ README và xem hướng dẫn đóng góp lần đầu của chúng tôi để biết các bước chung.
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của TanStack/redact
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 84/100
TanStack/redact#142 · 2 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 85/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 55/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 5/5 Hơn một tuần Mức phù hợp với người mới 35/100
Maintainer thường phản hồi trong vòng 1 ngày
Tất cả issue của TanStack/redact
Issue tương tự
-
[fullsend] E2E: scorecard — filecheck GitLab entity metrics unavailable due to initialDelay timingĐang mởe2e-failure ready-to-code
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 75/100
redhat-developer/rhdh-plugin-export-overlays#4698 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 88/100
devswha/herdr-web-ui#2884 ·
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
Maintainer thường phản hồi trong vòng 1 ngày
-
b:minor bug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
software-mansion/argent#1771 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Resources: New palettes of XiamenCó thể đã có người làm @rmt-svc đã nhận hôm nay. Đang mởresources
Độ khó 1/5 1-3 giờ Mức phù hợp với người mới 74/100
railmapgen/rmg-palette#2491 ·
Maintainer thường phản hồi trong vòng 4 ngày