Skip to content

feat(web): add MobX app shell store with substores#1204

Merged
cungminh2710 merged 3 commits into
mainfrom
cursor/mobx-app-shell-store-cd25
Jul 4, 2026
Merged

feat(web): add MobX app shell store with substores#1204
cungminh2710 merged 3 commits into
mainfrom
cursor/mobx-app-shell-store-cd25

Conversation

@cungminh2710

Copy link
Copy Markdown
Contributor

What changed

Adds a MobX AppShellStore scoped to the authenticated org layout, with four substores:

  • SidebarStore — bridges to SidebarProvider for imperative open/close, forceCollapsed, and preferredOpen
  • NavigationStore — route-driven nav by default; pages can switch to custom nav groups via useAppShellNavigationCustom
  • BreadcrumbStore — layers overrides/appends on top of URL-derived breadcrumbs from getAppShellBreadcrumbs
  • HeaderActionsStore — slot registry for navbar actions rendered before TMS connect / theme / user menu

The store resets page-scoped state (actions, breadcrumb overrides, custom nav, sidebar preferences) on every route change.

New hooks

Hook Purpose
useAppShellHeaderAction Register a navbar action slot
useAppShellBreadcrumbOverride Replace a crumb by index or segment match
useAppShellBreadcrumbAppend Append a trailing crumb
useAppShellNavigationCustom Replace sidebar nav with custom groups
useAppShellSidebar Control sidebar open state / force collapse

Architecture notes

  • URL + React Query remain the source of truth for routing and server data; MobX coordinates page-driven shell customization only
  • Store is created per layout mount (not a global singleton), matching the CAT workspace pattern
  • observer is limited to shell chrome components (AppShellBreadcrumb, AppShellNavigation, AppShellHeaderActions, SidebarStoreBridge)

How to test

cd apps/hyperlocalise-web
vp check --fix
vp test src/components/app-shell

Example page usage:

useAppShellHeaderAction({
  id: "save",
  order: 10,
  render: () => <Button onClick={save}>Save</Button>,
});

useAppShellBreadcrumbOverride({
  id: "job-title",
  matchSegment: "jobs",
  label: job.title,
});

useAppShellSidebar({ forceCollapsed: true });

Checklist

  • I ran relevant checks locally (vp check --fix, vp test src/components/app-shell)
  • I updated docs, comments, or examples when needed
  • This PR is scoped and ready for review
Open in Web Open in Cursor 

Introduce AppShellStore with sidebar, navigation, breadcrumb, and
header action substores. Wire the store into AppShellClient so pages
can register navbar actions, override breadcrumbs, customize sidebar
navigation, and control sidebar open state via hooks.

Co-authored-by: Minh Cung <cungminh2710@users.noreply.github.com>
@vercel

vercel Bot commented Jul 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
hyperlocalise Ignored Ignored Jul 4, 2026 12:58am

Request Review

@cungminh2710
cungminh2710 marked this pull request as ready for review July 4, 2026 00:33
@cungminh2710
cungminh2710 requested a review from MuenYu as a code owner July 4, 2026 00:33
@cursor

cursor Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@greptile-apps

greptile-apps Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces a MobX AppShellStore scoped to the authenticated org layout, composing four substores — SidebarStore, NavigationStore, BreadcrumbStore, and HeaderActionsStore — alongside five React hooks for page-level shell customisation. The architecture is clean: store creation is once-per-layout-mount via useState, route-change resets happen during the render phase (well-commented to explain the passive-effect ordering tradeoff), and observer is correctly limited to the four shell chrome components.

  • The render-time resetPageScope() guard using a useRef correctly handles Strict Mode double-invocation and concurrent render restarts.
  • buildGroupsSignature stabilises useAppShellNavigationCustom deps against inline-array recreation, but only hashes href and label — icon or badge changes won't trigger a re-register without a fresh reference.
  • SidebarStore computed getters (isOpen, isMobile, state) delegate to a plain-JS api object that MobX cannot track; reactive reads in observer consumers will lag behind actual sidebar state until SidebarStoreBridge re-renders and re-binds.

Confidence Score: 5/5

Safe to merge; all changes are additive shell-coordination infrastructure with no mutations to routing, auth, or data-fetching logic.

The store is well-tested, the observer scope is narrow and intentional, and the render-time reset has a clear rationale. The two findings are design-time tradeoffs that don't break current usage: the stale computed getters in SidebarStore are not read reactively by any existing consumer in this PR, and the partial navigation signature only matters if callers pass dynamically-mutating icon references (not present yet).

sidebar-store.ts (reactive-read correctness of isOpen/isMobile/state) and use-app-shell-navigation.ts (buildGroupsSignature completeness).

Important Files Changed

Filename Overview
apps/hyperlocalise-web/src/components/app-shell/store/sidebar-store.ts Computed getters isOpen/isMobile/state read plain-JS api properties MobX cannot track; reactive reads in observer components will lag behind sidebar state changes until the next bindSidebarApi call.
apps/hyperlocalise-web/src/components/app-shell/store/sidebar-store-bridge.tsx Two-effect design: one for bind, one for sync; sidebar object in bind deps correctly rebinds on SidebarProvider re-renders.
apps/hyperlocalise-web/src/components/app-shell/store/use-app-shell-navigation.ts Uses buildGroupsSignature + refs to stabilise deps, but signature only hashes group.label / item.href / item.label — icon changes won't re-register the custom navigation.
apps/hyperlocalise-web/src/components/app-shell/store/app-shell-store-context.tsx Provider creates store once via useState; ref-guarded render-time reset avoids passive-effect ordering issues. Clean implementation.
apps/hyperlocalise-web/src/components/app-shell/store/app-shell-store.ts Root store composing four substores; resetPageScope correctly delegates to each. makeAutoObservable with autoBind is appropriate.
apps/hyperlocalise-web/src/components/app-shell/store/app-shell-store.test.ts Good unit coverage of all four substores; uses vite-plus/test correctly per project conventions.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Page as Page Component
    participant Bridge as SidebarStoreBridge
    participant Provider as AppShellStoreProvider
    participant Store as AppShellStore
    participant Sidebar as SidebarProvider

    Note over Provider: Mount - creates store via useState()
    Provider->>Store: createAppShellStore(defaultNavigationGroups)

    Note over Bridge: Mount (child of SidebarProvider)
    Bridge->>Sidebar: useSidebar()
    Bridge->>Store: sidebar.bindSidebarApi(api)
    Store->>Store: sync()

    Note over Page: Page hooks fire (deepest children first)
    Page->>Store: sidebar.setForceCollapsed(true)
    Store->>Store: sync() → api is null, no-op
    Page->>Store: navigation.setCustomNavigation(groups)
    Page->>Store: breadcrumb.registerOverride(...)
    Page->>Store: headerActions.register(slot)

    Note over Bridge: bind effect runs after page effects
    Bridge->>Store: sidebar.bindSidebarApi(sidebar)
    Store->>Sidebar: setOpen(false) / sync forced state

    Note over Provider: Route change detected during render
    Provider->>Store: resetPageScope()
    Store->>Store: breadcrumb.clearOverrides()
    Store->>Store: headerActions.clearAll()
    Store->>Store: navigation.clearCustomMode()
    Store->>Store: sidebar.setForceCollapsed(false)
    Store->>Store: sidebar.setPreferredOpen(null)
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Page as Page Component
    participant Bridge as SidebarStoreBridge
    participant Provider as AppShellStoreProvider
    participant Store as AppShellStore
    participant Sidebar as SidebarProvider

    Note over Provider: Mount - creates store via useState()
    Provider->>Store: createAppShellStore(defaultNavigationGroups)

    Note over Bridge: Mount (child of SidebarProvider)
    Bridge->>Sidebar: useSidebar()
    Bridge->>Store: sidebar.bindSidebarApi(api)
    Store->>Store: sync()

    Note over Page: Page hooks fire (deepest children first)
    Page->>Store: sidebar.setForceCollapsed(true)
    Store->>Store: sync() → api is null, no-op
    Page->>Store: navigation.setCustomNavigation(groups)
    Page->>Store: breadcrumb.registerOverride(...)
    Page->>Store: headerActions.register(slot)

    Note over Bridge: bind effect runs after page effects
    Bridge->>Store: sidebar.bindSidebarApi(sidebar)
    Store->>Sidebar: setOpen(false) / sync forced state

    Note over Provider: Route change detected during render
    Provider->>Store: resetPageScope()
    Store->>Store: breadcrumb.clearOverrides()
    Store->>Store: headerActions.clearAll()
    Store->>Store: navigation.clearCustomMode()
    Store->>Store: sidebar.setForceCollapsed(false)
    Store->>Store: sidebar.setPreferredOpen(null)
Loading

Reviews (3): Last reviewed commit: "fix(web): reset app shell store during r..." | Re-trigger Greptile

Comment thread apps/hyperlocalise-web/src/components/app-shell/store/app-shell-store-context.tsx Outdated
cursoragent and others added 2 commits July 4, 2026 00:50
Stabilize navigation custom hook deps with refs and primitive
signatures, use useState for store init, move sidebar hook to its own
file, and stop re-syncing sidebar on provider re-renders.

Co-authored-by: Minh Cung <cungminh2710@users.noreply.github.com>
Move pathname reset out of useEffect so it runs before child page
hooks register shell chrome state. Passive effects flush bottom-up,
which left header actions and breadcrumb overrides cleared for the
entire page lifetime.

Co-authored-by: Minh Cung <cungminh2710@users.noreply.github.com>
@cungminh2710
cungminh2710 merged commit 76337cd into main Jul 4, 2026
9 checks passed
@cungminh2710
cungminh2710 deleted the cursor/mobx-app-shell-store-cd25 branch July 4, 2026 01:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants