From d7ce732d8a9187400c19a71e5bb8fd6c69ab79f1 Mon Sep 17 00:00:00 2001 From: guanbinrui Date: Wed, 30 Sep 2026 13:06:14 +0800 Subject: [PATCH] fix: react #467 crash in useValueRef on suspense replay useValueRef only called `use(readyPromise)` while the ref was not ready. When a component suspended there and was replayed after the promise resolved, `ready` had flipped to true so `use` was skipped; React only switches back to the mount dispatcher inside `use`, so the following useSyncExternalStore threw "Update hook called on initial render" (crashing the Mask Network entrance in the X sidebar via ToolboxHintForWallet -> useGuideStepState). - Always call `use(readyPromise)` for ValueRefWithReady. - Mark readyPromise as fulfilled (React thenable protocol) so `use` returns synchronously once ready instead of suspending briefly. Co-Authored-By: Claude Opus 5.5 --- packages/shared-base-ui/src/hooks/useValueRef.ts | 8 ++++++-- packages/shared-base/src/helpers/ValueRef.ts | 5 ++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/shared-base-ui/src/hooks/useValueRef.ts b/packages/shared-base-ui/src/hooks/useValueRef.ts index fa722c806bf..3373fa47fae 100644 --- a/packages/shared-base-ui/src/hooks/useValueRef.ts +++ b/packages/shared-base-ui/src/hooks/useValueRef.ts @@ -7,8 +7,12 @@ function getServerSnapshot(): never { } export function useValueRef(ref: ValueRef): T { if ('readyPromise' in ref) { - const ref2 = ref as ValueRefWithReady - if (!ref2.ready) use(ref2.readyPromise) + // Must NOT be conditional on `ref.ready`. When a component suspends here and is replayed + // after the promise resolves, React only switches from the update dispatcher back to the + // mount dispatcher inside `use`. Skipping `use` on replay (because `ready` flipped to true) + // makes the following hooks throw "Update hook called on initial render" (React #467). + // `use` on an already-resolved promise returns synchronously once React has tracked it. + use((ref as ValueRefWithReady).readyPromise) } return useSyncExternalStore( (f) => ref.addListener(f), diff --git a/packages/shared-base/src/helpers/ValueRef.ts b/packages/shared-base/src/helpers/ValueRef.ts index 13df0817a6a..593804dc8e1 100644 --- a/packages/shared-base/src/helpers/ValueRef.ts +++ b/packages/shared-base/src/helpers/ValueRef.ts @@ -36,7 +36,10 @@ export class ValueRefWithReady extends ValueRef { // this is unsafe. we assigned T | undefined to T super(value!, isEqual) const { promise, resolve } = Promise.withResolvers() - this.readyPromise = promise.then(() => this.value) + const readyPromise = promise.then(() => this.value) + // Follow React's thenable protocol so `use(readyPromise)` returns synchronously once ready. + readyPromise.then((value) => Object.assign(readyPromise, { status: 'fulfilled', value })) + this.readyPromise = readyPromise this.#nowReady = resolve }