You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
2.0 rc: subFetch swaps the global Promise during hydration; user-cached import() becomes a never-settling MockPromise #4332
Versions:solid-js 2.0.0-rc.18 (also present in 2.0.0-rc.19: dist/solid.js:429), @solidjs/web 2.0.0-rc.18, Vite 8.3.5.
Related: #4013, fixed by #4019 for lazy()/lazyModule(). This is the same root cause reaching user code that #4019 doesn't cover.
What happens
While hydration traces an adopted async computation, subFetch (dist/solid.js:422) sets the globalPromise = MockPromise (:428) and window.fetch to return a MockPromise, runs the computation, then restores both. MockPromise's then/catch/finally/resolve/all return new mock promises that never settle.
Any user code that creates a promise during that window and keeps it gets a promise that never resolves. A common case is a module-level de-duplication cache for lazy imports. Vite's __vitePreload, which wraps every built import(), starts with let promise = Promise.resolve(), reading the global at call time, so the stored import() is a MockPromise.
Repro
// messages.jsconstloads=newMap()exportfunctionload(key){letp=loads.get(key)if(!p)loads.set(key,(p=import("./catalog.js")))// built: goes through __vitePreloadreturnp}// component (SSR'd inside <Loading>, then hydrated)constdata=createMemo(()=>load("x"))
SSR the component inside <Loading> so the memo's value is serialized, then hydrate.
During hydration the memo runs only to track reads, under subFetch, so load("x") stores a MockPromise in loads.
Unmount, then mount another component that calls load("x").
Expected: it resolves with the module. Actual: it stays pending forever. No request for catalog.js is made, and in an app this froze client navigation.
Suggested fix
Don't replace globals. Pass the mock explicitly where Solid needs it, or restore the globals before user code can create promises it keeps. At minimum, document that promises created inside a computation while it hydrates are fake and must not be cached.
Workaround
On the client, when the memo already holds the hydrated (serialized) value, write it into the cache, so later callers never touch the stored mock promise.
Thanks for the detailed report. This is working as designed. When a computation adopts the server's serialized value during hydration, the client still runs it once to record its dependencies. During that run, Promise and fetch are stubbed so that nothing is requested a second time. Without the stubs, the duplicate requests on load would come back, so we're keeping them. lazy() already avoids caching a load made during that run (#4019).
The part that hangs is user code holding on to a promise created inside the compute, which includes an import() that goes through Vite's __vitePreload. We've documented the rule and its alternatives: cache the resolved value instead of the promise, start the load outside the compute, use lazy() for components, or for a data library, skip caching while isHydrating() and seed from takeHydrationValue.
Activity
docs: onFlush and the hydration tracking run in the skill and cheatsh…
ryansolid commentedon Oct 28, 2026
Thanks for the detailed report. This is working as designed. When a computation adopts the server's serialized value during hydration, the client still runs it once to record its dependencies. During that run,
Promiseandfetchare stubbed so that nothing is requested a second time. Without the stubs, the duplicate requests on load would come back, so we're keeping them.lazy()already avoids caching a load made during that run (#4019).The part that hangs is user code holding on to a promise created inside the compute, which includes an
import()that goes through Vite's__vitePreload. We've documented the rule and its alternatives: cache the resolved value instead of the promise, start the load outside the compute, uselazy()for components, or for a data library, skip caching whileisHydrating()and seed fromtakeHydrationValue.documentation/solid-2.0/05-async-data.md"The tracking run: known limitations", plus thesolid-2.0skill and CHEATSHEET (docs: onFlush and the hydration tracking run in the skill and cheatsheet; cached import() note (#4332) #4335)Closing as not planned for the runtime. The docs part is done.
Generated by Claude Code