Thanks to visit codestin.com
Credit goes to github.com

Skip to content

lifecycleCache / context.state not available in renderStage="client" widgets #720

@aui

Description

@aui

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() with expose: true

Steps to reproduce

  1. Configure a route with i18n middleware that calls lifecycleCache().set('i18n:config', ..., true) (and similar for globalization context).
  2. In the route component, render a widget with renderStage="client" that uses translation('someNamespace') (or any API that reads from lifecycleCache() / context().state).
  3. 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

  1. Serialization is tied to “boundaries that actually render”
    In @web-widget/web-widget server, renderLifecycleCacheLayer() is only called after a widget is rendered (in renderInnerHTMLToString()). When renderStage === "client", the method returns early and never calls renderLifecycleCacheLayer(), so no script is emitted for that slot.

  2. Page-level state is never pushed
    Request-level data (middleware, route, etc.) lives in the same context().state that would be serialized by renderLifecycleCacheLayer(). 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 in self[LIFECYCLE_CACHE_LAYER].

  3. Client-only widgets get a fresh, empty context
    When a client-only widget mounts, it uses a new context with empty state. The framework does not call mountLifecycleCacheLayer for that boundary (or pre-fill state from the layer), so lifecycleCache() 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-widget server: ServerWebWidgetRenderer.renderInnerHTMLToString() – early return for renderStage === "client" without calling renderLifecycleCacheLayer()

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions