-
Notifications
You must be signed in to change notification settings - Fork 4
Description
Summary
Widgets with renderStage="client" cannot access request-level data that was stored in context.state and exposed via lifecycleCache().set(key, value, true) (e.g. i18n config, user context). They receive an empty context().state on the client because the page-level state is never serialized when client-only widgets are present.
Environment
@web-widget/web-widget(and related packages) @ 1.63.0- Meta-framework SSR with React/Vue; middleware writes to
lifecycleCache()withexpose: true
Steps to reproduce
- Configure a route with i18n middleware that calls
lifecycleCache().set('i18n:config', ..., true)(and similar for globalization context). - In the route component, render a widget with
renderStage="client"that usestranslation('someNamespace')(or any API that reads fromlifecycleCache()/context().state). - Load the page in the browser.
Expected: The client-only widget can read the same page-level cache (e.g. i18n) as server-rendered content.
Actual: The widget throws (e.g. I18nKitError: i18next instance not found or equivalent), because lifecycleCache().get(...) returns undefined. The widget’s context().state is empty and self[LIFECYCLE_CACHE_LAYER] has no (or incomplete) page-level data.
Root cause
-
Serialization is tied to “boundaries that actually render”
In@web-widget/web-widgetserver,renderLifecycleCacheLayer()is only called after a widget is rendered (inrenderInnerHTMLToString()). WhenrenderStage === "client", the method returns early and never callsrenderLifecycleCacheLayer(), so no script is emitted for that slot. -
Page-level state is never pushed
Request-level data (middleware, route, etc.) lives in the samecontext().statethat would be serialized byrenderLifecycleCacheLayer(). If the only boundaries that run on the server are client-only widgets (which return before calling it) or if the “page” boundary never triggers a push, that state is never serialized, so the client never gets it inself[LIFECYCLE_CACHE_LAYER]. -
Client-only widgets get a fresh, empty context
When a client-only widget mounts, it uses a new context with emptystate. The framework does not callmountLifecycleCacheLayerfor that boundary (or pre-fill state from the layer), solifecycleCache()there has nothing to read.
So the issue is both “client-only context is empty” and “page-level state never gets a chance to be serialized.”
Suggested fixes (in @web-widget)
Option A – Serialize when handling renderStage="client" (minimal change)
In @web-widget/web-widget server’s renderInnerHTMLToString(), when renderStage === "client", before returning the empty string, call renderLifecycleCacheLayer() (with current context().state) and append the result to the widget’s inner HTML. That way each client-only widget slot emits a script that pushes the page-level state to self[LIFECYCLE_CACHE_LAYER], and client-only widgets can rely on a client-side fallback that reads from the layer when context().state is missing a key.
Option B – Push page-level state once before the first widget
At the start of the first widget’s renderInnerHTMLToString() (or at a single “document/root” serialization point), call renderLifecycleCacheLayer(context().state) once per request (e.g. guarded by a request-scoped flag like context().state.__pageLayerPushed__) so the layer always contains page-level data before any widget runs.
Option C – Give client-only boundaries access to the layer on mount
When mounting a renderStage="client" widget, call mountLifecycleCacheLayer for that boundary (or pre-fill its context’s state from self[LIFECYCLE_CACHE_LAYER]) so lifecycleCache() inside the widget can read page-level data. This still requires that page-level state is serialized at least once (e.g. via A or B).
We have applied Option A locally (and optionally a client-side get fallback in @web-widget/lifecycle-cache) and it resolves the issue without requiring app-level workarounds.
References
- lifecycle cache:
renderLifecycleCacheLayer(server),mountLifecycleCacheLayer(client),self[LIFECYCLE_CACHE_LAYER] @web-widget/web-widgetserver:ServerWebWidgetRenderer.renderInnerHTMLToString()– early return forrenderStage === "client"without callingrenderLifecycleCacheLayer()