feat(lightspeed): replace Context-based drawer state with global store#3697
feat(lightspeed): replace Context-based drawer state with global store#3697its-mitesh-kumar wants to merge 2 commits into
Conversation
Remove legacy (OFS) re-exports from the main ./ entry point — they are now exclusively available at ./legacy. Refactor drawer state management from a globalThis React Context singleton to a proper global store using @backstage/version-bridge + useSyncExternalStore, eliminating the Provider dependency for NFS consumers. BREAKING CHANGE: Legacy component imports must now use the ./legacy subpath. Signed-off-by: its-mitesh-kumar <itsmiteshkumar98@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
|
Important This PR includes changes that affect public-facing API. Please ensure you are adding/updating documentation for new features or behavior. Changed Packages
|
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #3697 +/- ##
==========================================
+ Coverage 54.31% 54.34% +0.02%
==========================================
Files 2349 2349
Lines 89701 89753 +52
Branches 25105 25121 +16
==========================================
+ Hits 48723 48778 +55
+ Misses 40748 40745 -3
Partials 230 230
*This pull request uses carry forward flags. Click here to find out more. Continue to review full report in Codecov by Harness.
🚀 New features to boost your workflow:
|
…provisional The synchronous store update via useSyncExternalStore triggers a re-render before local conversationId state updates, causing the provisional-thread detection effect to re-disable the New Chat button after onComplete already enabled it. Add an else branch to reset newChatCreated=false once the thread has a real ID. Signed-off-by: its-mitesh-kumar <itsmiteshkumar98@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
|
|
Closing this in favor of #3708. |



Description
Remove legacy (OFS) component re-exports from the main
./entry point — they are now exclusively available at the./legacysubpath. Refactors the drawer state management from aglobalThisReact Context singleton to a proper global store using@backstage/version-bridge+useSyncExternalStore, eliminating the Provider dependency for NFS consumers and resolving cross-MF-boundary state sharing issues cleanly.Key Changes
./entry point (breaking) — consumers must use./legacylightspeedDrawerStore.tsprovides a singleton store via@backstage/version-bridgeuseLightspeedDrawerhook replacesuseLightspeedDrawerContextusinguseSyncExternalStoreLightspeedDrawerProvidersimplified to a router-bridge shell (no Context.Provider)Fixed
Architecture: globalThis → version-bridge Global Store
What globalThis was solving
In Module Federation (NFS), each dynamically loaded plugin remote executes in its own JavaScript module scope. When the Lightspeed plugin's code is split across multiple remotes (main plugin, FAB module, Translations module), each remote runs its own copy of
LightspeedDrawerContext.tsx. This meanscreateContext()is called multiple times, producing duplicate React Context objects:createContext()→Context_AcreateContext()→Context_BThe FAB component uses
Context_Ato read state, butLightspeedDrawerProviderprovides values onContext_B. They never see each other → "must be used within a LightspeedDrawerProvider" error.The globalThis fix (PR #3513) solved this by storing the Context in a well-known global key:
This ensured all remotes shared the same Context object, so Provider and consumers could communicate.
Problems with globalThis approach
Still requires Provider nesting — Even with a shared Context, a
<LightspeedDrawerProvider>must wrap all consumers in the React tree. In NFS, the FAB mounts viaAppRootWrapperBlueprintand the drawer content viaAppDrawerContentBlueprint— they're siblings, not parent-child. Ensuring the Provider wraps both depends on extension load order, which is fragile.Race condition risk — If any remote calls
getOrCreateContext()before the "canonical" one, it wins. The Context is created without any guarantee about which remote established it first.No version safety — If two different plugin versions are loaded (e.g., during a rolling upgrade),
globalThis['__lightspeed_drawer_context__']silently collides with no type safety or version negotiation.Invisible to React DevTools — A raw
globalThiskey doesn't appear in React's component tree or DevTools Context inspector, making debugging harder.Not aligned with Backstage conventions — Backstage has a purpose-built utility (
@backstage/version-bridge) for exactly this problem, but it wasn't being used.How version-bridge + useSyncExternalStore solves it
The new approach eliminates React Context entirely for state management:
Why this is better:
How the data flows:
No Provider in between. No Context. No ordering dependency. Just a shared store that any component in any remote can subscribe to.
Test it on RHDH-Local
Prerequisites
.envof RHDH-localapp-root-wrapper:app/drawer)1. Build the Lightspeed dynamic plugin
2. Configure dynamic plugins
In
dynamic-plugins.override.yaml:3. Configure NFS extensions
In
configs/app-config/app-config.local.yaml, use either option:4. Start RHDH-local
cd /path/to/rhdh-local podman compose down podman compose up -d5. Verify
✔️ Checklist