Skip to content

feat(app-defaults): replace drawer context with global store#2691

Merged
debsmita1 merged 1 commit intoredhat-developer:mainfrom
rohitkrai03:nfs-header
Apr 7, 2026
Merged

feat(app-defaults): replace drawer context with global store#2691
debsmita1 merged 1 commit intoredhat-developer:mainfrom
rohitkrai03:nfs-header

Conversation

@rohitkrai03
Copy link
Copy Markdown
Contributor

Hey, I just made a Pull Request!

  • Replaced React Context based state for Drawer with global store using version-bridge packages' internal store utility.
  • Re-organised the app-react plugin files/folders a bit to make sure it scales with new features in future.

✔️ Checklist

  • A changeset describing the change and affected packages. (more info)
  • Added or Updated documentation
  • Tests for new functionality and regression tests for bug fixes

@rohitkrai03 rohitkrai03 requested review from a team and christoph-jerolimov as code owners April 2, 2026 13:32
@rohitkrai03 rohitkrai03 requested a review from HusneShabbir April 2, 2026 13:32
@rhdh-gh-app
Copy link
Copy Markdown

rhdh-gh-app bot commented Apr 2, 2026

Changed Packages

Package Name Package Path Changeset Bump Current Version
app workspaces/app-defaults/packages/app none v0.0.0
@red-hat-developer-hub/backstage-plugin-app-react workspaces/app-defaults/plugins/app-react patch v0.0.2

@rhdh-qodo-merge
Copy link
Copy Markdown

Review Summary by Qodo

Replace drawer context with global store using version-bridge

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Replaced React Context with global store for drawer state management
• Removed AppDrawerProvider requirement, enabling drawer usage anywhere
• Reorganized drawer module structure into components, hooks, and utilities
• Added @backstage/version-bridge dependency for singleton store pattern
• Updated tests to work without provider wrapper
• Added demo drawer items to sidebar for testing
Diagram
flowchart LR
  A["AppDrawerContext<br/>React Context"] -->|"Replaced with"| B["drawerStore<br/>Global Singleton"]
  B -->|"Accessed via"| C["useAppDrawer Hook<br/>useSyncExternalStore"]
  D["AppDrawerProvider<br/>Removed"] -->|"No longer needed"| C
  E["Reorganized Files<br/>components/hooks/utils"] -->|"Cleaner structure"| F["Scalable architecture"]
Loading

Grey Divider

File Changes

1. workspaces/app-defaults/plugins/app-react/src/alpha.ts 📝 Documentation +2/-3

Updated package documentation scope

workspaces/app-defaults/plugins/app-react/src/alpha.ts


2. workspaces/app-defaults/plugins/app-react/src/drawer/index.ts ✨ Enhancement +9/-5

Reorganized exports with new folder structure

workspaces/app-defaults/plugins/app-react/src/drawer/index.ts


3. workspaces/app-defaults/plugins/app-react/src/drawer/utils/drawerStore.ts ✨ Enhancement +91/-0

Created global drawer store with singleton pattern

workspaces/app-defaults/plugins/app-react/src/drawer/utils/drawerStore.ts


View more (13)
4. workspaces/app-defaults/plugins/app-react/src/drawer/hooks/useAppDrawer.tsx ✨ Enhancement +43/-0

Implemented hook using useSyncExternalStore

workspaces/app-defaults/plugins/app-react/src/drawer/hooks/useAppDrawer.tsx


5. workspaces/app-defaults/plugins/app-react/src/drawer/AppDrawerContext.tsx ✨ Enhancement +0/-121

Removed React Context implementation

workspaces/app-defaults/plugins/app-react/src/drawer/AppDrawerContext.tsx


6. workspaces/app-defaults/plugins/app-react/src/drawer/extensions/appDrawerContentDataRef.ts ✨ Enhancement +1/-1

Updated import path to new structure

workspaces/app-defaults/plugins/app-react/src/drawer/extensions/appDrawerContentDataRef.ts


7. workspaces/app-defaults/plugins/app-react/src/drawer/components/ApplicationDrawer.tsx ✨ Enhancement +2/-2

Updated imports for new folder organization

workspaces/app-defaults/plugins/app-react/src/drawer/components/ApplicationDrawer.tsx


8. workspaces/app-defaults/plugins/app-react/src/drawer/components/ApplicationDrawer.test.tsx 🧪 Tests +6/-8

Removed provider wrapper from tests

workspaces/app-defaults/plugins/app-react/src/drawer/components/ApplicationDrawer.test.tsx


9. workspaces/app-defaults/plugins/app-react/src/drawer/hooks/useAppDrawer.test.tsx 🧪 Tests +18/-9

Updated tests for global store implementation

workspaces/app-defaults/plugins/app-react/src/drawer/hooks/useAppDrawer.test.tsx


10. workspaces/app-defaults/plugins/app-react/src/drawer/extensions/appDrawerModule.tsx ✨ Enhancement +2/-5

Removed provider from module wrapper

workspaces/app-defaults/plugins/app-react/src/drawer/extensions/appDrawerModule.tsx


11. workspaces/app-defaults/plugins/app-react/src/extensions/index.ts ✨ Enhancement +0/-19

Removed extensions barrel export file

workspaces/app-defaults/plugins/app-react/src/extensions/index.ts


12. workspaces/app-defaults/plugins/app-react/package.json Dependencies +1/-0

Added version-bridge dependency

workspaces/app-defaults/plugins/app-react/package.json


13. workspaces/app-defaults/packages/app/src/modules/nav/Sidebar.tsx ✨ Enhancement +23/-2

Added demo drawer items for testing

workspaces/app-defaults/packages/app/src/modules/nav/Sidebar.tsx


14. workspaces/app-defaults/plugins/app-react/src/drawer/components/DrawerPanel.test.tsx Additional files +0/-0

...

workspaces/app-defaults/plugins/app-react/src/drawer/components/DrawerPanel.test.tsx


15. workspaces/app-defaults/plugins/app-react/src/drawer/components/DrawerPanel.tsx Additional files +0/-0

...

workspaces/app-defaults/plugins/app-react/src/drawer/components/DrawerPanel.tsx


16. workspaces/app-defaults/plugins/app-react/src/drawer/extensions/AppDrawerContentBlueprint.ts Additional files +0/-0

...

workspaces/app-defaults/plugins/app-react/src/drawer/extensions/AppDrawerContentBlueprint.ts


Grey Divider

Qodo Logo

@rhdh-qodo-merge
Copy link
Copy Markdown

rhdh-qodo-merge bot commented Apr 2, 2026

Code Review by Qodo

🐞 Bugs (4) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX Issues (0)

Grey Divider


Action required

1. Drawer state leaks between tests 🐞 Bug ☼ Reliability
Description
After removing the provider wrapper in ApplicationDrawer.test.tsx, drawer state now lives in a
global singleton and is not reset between tests. Tests that open a drawer can leave activeDrawerId
set, making subsequent tests order-dependent/flaky.
Code

workspaces/app-defaults/plugins/app-react/src/drawer/components/ApplicationDrawer.test.tsx[R35-38]

+    <ApplicationDrawer contents={contents}>
+      <OpenButton id="test-drawer" />
+      <CloseButton id="test-drawer" />
+    </ApplicationDrawer>,
Evidence
The drawer store is a global singleton, so unmounting components between tests does not reset it.
ApplicationDrawer.test.tsx opens a drawer in some tests without a corresponding global store
reset, while other tests expect no active drawer.

workspaces/app-defaults/plugins/app-react/src/drawer/utils/drawerStore.ts[81-91]
workspaces/app-defaults/plugins/app-react/src/drawer/components/ApplicationDrawer.test.tsx[33-40]
workspaces/app-defaults/plugins/app-react/src/drawer/components/ApplicationDrawer.test.tsx[62-74]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Drawer tests became susceptible to cross-test state leakage because the drawer state is now stored in a global singleton and `ApplicationDrawer.test.tsx` does not reset it.

### Issue Context
`useAppDrawer.test.tsx` already implements a local `resetStore()` helper, but `ApplicationDrawer.test.tsx` does not.

### Fix Focus Areas
- workspaces/app-defaults/plugins/app-react/src/drawer/components/ApplicationDrawer.test.tsx[33-109]
- workspaces/app-defaults/plugins/app-react/src/drawer/hooks/useAppDrawer.test.tsx[22-35]
- workspaces/app-defaults/plugins/app-react/src/drawer/utils/drawerStore.ts[27-79]

### Suggested fix
Add a `beforeEach` (or `afterEach`) in `ApplicationDrawer.test.tsx` that resets the global drawer store state.

Preferred:
- Add an internal `reset()` method on `drawerStore` (clears `activeDrawerId` and `widths`) and use it in both test files.

Minimal alternative:
- Import `drawerStore` into `ApplicationDrawer.test.tsx` and close any active drawer in `beforeEach` similar to `useAppDrawer.test.tsx`.

Ensure the reset also clears widths if any tests set widths, to prevent future flakiness.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Snapshot bypass in hook 🐞 Bug ≡ Correctness
Description
useAppDrawer() subscribes with useSyncExternalStore, but returns isOpen/getWidth as direct
store methods that read the mutable store state instead of the captured snapshot. This can produce
inconsistent results within a render (e.g., activeDrawerId from the snapshot but
isOpen()/getWidth() computed from a newer store state).
Code

workspaces/app-defaults/plugins/app-react/src/drawer/hooks/useAppDrawer.tsx[R28-42]

+export const useAppDrawer = (): AppDrawerApi => {
+  const state = useSyncExternalStore(
+    drawerStore.subscribe,
+    drawerStore.getSnapshot,
+  );
+
+  return {
+    activeDrawerId: state.activeDrawerId,
+    isOpen: drawerStore.isOpen,
+    openDrawer: drawerStore.openDrawer,
+    closeDrawer: drawerStore.closeDrawer,
+    toggleDrawer: drawerStore.toggleDrawer,
+    getWidth: drawerStore.getWidth,
+    setWidth: drawerStore.setWidth,
+  };
Evidence
The hook captures a snapshot via useSyncExternalStore(...), but exposes
drawerStore.isOpen/drawerStore.getWidth which directly read the internal mutable state
variable inside the store, bypassing the subscribed snapshot.

workspaces/app-defaults/plugins/app-react/src/drawer/hooks/useAppDrawer.tsx[28-42]
workspaces/app-defaults/plugins/app-react/src/drawer/utils/drawerStore.ts[27-70]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`useAppDrawer()` mixes snapshot-derived state (`activeDrawerId`) with store methods (`isOpen`, `getWidth`) that read the current mutable store state, bypassing the snapshot returned by `useSyncExternalStore`.

### Issue Context
This can lead to briefly inconsistent results during rendering because `isOpen()` / `getWidth()` are not guaranteed to align with the snapshot used for `activeDrawerId`.

### Fix Focus Areas
- workspaces/app-defaults/plugins/app-react/src/drawer/hooks/useAppDrawer.tsx[28-42]
- workspaces/app-defaults/plugins/app-react/src/drawer/utils/drawerStore.ts[19-78]

### Suggested fix
In `useAppDrawer`, implement `isOpen` and `getWidth` as closures over the `state` snapshot (optionally memoized with `useCallback`). Keep mutators (`openDrawer`, `closeDrawer`, `toggleDrawer`, `setWidth`) as store methods.

Example approach:
- `isOpen: (id) => state.activeDrawerId === id`
- `getWidth: (id) => state.widths.get(id) ?? DEFAULT_WIDTH`

If you want to avoid duplicating `DEFAULT_WIDTH`, export a shared constant from the store/util module and import it into the hook.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. No server snapshot provided 🐞 Bug ☼ Reliability
Description
useSyncExternalStore is called without the getServerSnapshot argument. If this hook is ever used
during server rendering, React will not have a deterministic server snapshot, risking hydration
mismatches or runtime warnings/errors.
Code

workspaces/app-defaults/plugins/app-react/src/drawer/hooks/useAppDrawer.tsx[R28-33]

+export const useAppDrawer = (): AppDrawerApi => {
+  const state = useSyncExternalStore(
+    drawerStore.subscribe,
+    drawerStore.getSnapshot,
+  );
+
Evidence
The hook calls useSyncExternalStore with only subscribe and getSnapshot (2 arguments) and does
not provide a server snapshot function.

workspaces/app-defaults/plugins/app-react/src/drawer/hooks/useAppDrawer.tsx[28-33]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`useSyncExternalStore` is invoked without the third `getServerSnapshot` parameter, which is the SSR-safe snapshot provider.

### Issue Context
Even if the app is currently client-only, this is a latent correctness/reliability issue if SSR or pre-rendering is introduced.

### Fix Focus Areas
- workspaces/app-defaults/plugins/app-react/src/drawer/hooks/useAppDrawer.tsx[28-33]
- workspaces/app-defaults/plugins/app-react/src/drawer/utils/drawerStore.ts[21-79]

### Suggested fix
Pass a third argument to `useSyncExternalStore`, e.g. a function that returns the initial drawer state for server rendering.

Example:
- `useSyncExternalStore(subscribe, getSnapshot, () => ({ activeDrawerId: null, widths: new Map() }))`

(Optionally centralize the initial state creation in the store util to avoid duplication.)

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Advisory comments

4. Misleading drawer module comment 🐞 Bug ⚙ Maintainability
Description
appDrawerModule.tsx documentation still claims it “provides the AppDrawerContext” and relies on a
context provider, but the provider was removed in this PR. This will mislead maintainers about why
the wrapper exists and how drawer state is propagated.
Code

workspaces/app-defaults/plugins/app-react/src/drawer/extensions/appDrawerModule.tsx[R44-49]

    const contents = inputs.drawers.map(d => d.get(appDrawerContentDataRef));
    return originalFactory({
      component: ({ children }) => (
-        <AppDrawerProvider>
-          <ApplicationDrawer contents={contents}>{children}</ApplicationDrawer>
-        </AppDrawerProvider>
+        <ApplicationDrawer contents={contents}>{children}</ApplicationDrawer>
      ),
    });
Evidence
The comment explicitly references AppDrawerContext and a provider, but the implementation now only
renders ApplicationDrawer directly, relying on the new global store rather than React context.

workspaces/app-defaults/plugins/app-react/src/drawer/extensions/appDrawerModule.tsx[26-37]
workspaces/app-defaults/plugins/app-react/src/drawer/extensions/appDrawerModule.tsx[43-50]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`appDrawerModule.tsx` has outdated documentation referencing `AppDrawerContext`/provider semantics that no longer exist after the global-store migration.

### Issue Context
The wrapper extension may still be needed to render `ApplicationDrawer` and wire NFS inputs, but it no longer "provides context".

### Fix Focus Areas
- workspaces/app-defaults/plugins/app-react/src/drawer/extensions/appDrawerModule.tsx[26-37]

### Suggested fix
Rewrite the comment to describe the current behavior:
- The module registers an app-root wrapper that renders `ApplicationDrawer`.
- Drawer state is provided by a global store (`useAppDrawer`) rather than a React context provider.
- Keep any NFS-related explanation that remains accurate.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud bot commented Apr 2, 2026

Copy link
Copy Markdown
Member

@debsmita1 debsmita1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/lgtm

@openshift-ci openshift-ci bot added the lgtm label Apr 7, 2026
@debsmita1 debsmita1 merged commit 5e9716e into redhat-developer:main Apr 7, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants