FIX: Deduplicate getExplorer and getDbExplorer API calls via Zustand store#71
Conversation
Both explorer-component.jsx and chat-ai/Body.jsx were independently calling explorerSvc.getExplorer(projectId), causing a duplicate GET /explorer request every time the chat drawer opened. Introduce a minimal Zustand store (explorer-store.js) that holds the shared response. explorer-component.jsx becomes the single writer (updates on every successful fetch, clears on project switch), and Body.jsx reads explorerData from the store to populate prompt autocomplete instead of issuing its own fetch. getDbExplorer in Body.jsx is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Extend useExplorerStore with a dbExplorerData slice so chat-ai/Body.jsx stops fetching /db_explorer independently. Previously, opening the chat drawer triggered a second identical GET /db_explorer alongside the one already issued by explorer-component.jsx on mount. explorer-component.jsx becomes the single writer for both slices (getExplorer and getDbExplorer wrappers), and the existing project-switch clear effect now resets both slices via an expanded clearExplorerData. Body.jsx subscribes to the store and mirrors dbExplorerData into the existing promptAutoComplete.dbData shape — no direct explorerService usage remains in chat-ai/Body.jsx (explorerService import, useRef import, and the explorerSvc ref were dropped). Also updated the PR description to cover both refactors in one PR. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
| Filename | Overview |
|---|---|
| frontend/src/store/explorer-store.js | New minimal Zustand store with explorerData, dbExplorerData, setters, and clearExplorerData — correct pattern, no persist middleware by design. |
| frontend/src/ide/explorer/explorer-component.jsx | Stores pre-mutation deep clone (rawTreeDataRef.current) in explorerData and raw res.data in dbExplorerData; project-switch clear uses prevProjectIdRef pattern; schemaMenu init changed to null to eliminate duplicate mount-time getExplorer call (still fires from previewTimeTravel effect on mount). |
| frontend/src/ide/chat-ai/Body.jsx | Removed direct explorerService calls; now passively mirrors explorerData/dbExplorerData from store into promptAutoComplete; explorerSvc ref and explorerService import cleaned up; comment documents the mount-order assumption. |
Sequence Diagram
sequenceDiagram
participant EC as explorer-component.jsx
participant ES as explorer-store.js (Zustand)
participant B as Body.jsx (chat-ai)
participant API as Backend API
EC->>API: GET /explorer (single fetch)
API-->>EC: res.data.children
EC->>EC: deep clone → rawTreeDataRef.current
EC->>ES: setExplorerData(rawTreeDataRef.current)
EC->>EC: mutate + transformTree (local only)
EC->>API: GET /db_explorer (single fetch)
API-->>EC: res.data
EC->>ES: setDbExplorerData(res.data)
ES-->>B: explorerData (reactive subscription)
B->>B: mirror → promptAutoComplete.modelsData/seedsData
ES-->>B: dbExplorerData (reactive subscription)
B->>B: mirror → promptAutoComplete.dbData
Note over EC,ES: On project switch
EC->>ES: clearExplorerData() (prevProjectIdRef guard)
ES-->>B: explorerData=null, dbExplorerData=null
B->>B: promptAutoComplete reset to {}
Reviews (4): Last reviewed commit: "fix: only clear explorer store on actual..." | Re-trigger Greptile
Three review-driven tweaks from the first round of review on this branch: - explorer-component.jsx: publish the raw /explorer response to useExplorerStore (via rawTreeDataRef.current) before the in-place mutations by sortModels, applyModelDecorations, and transformTree begin. The store now matches its documented contract and Body.jsx's autocomplete receives the untransformed children shape it did before the dedup refactor. - explorer-component.jsx: add clearExplorerData to the dependency array of the project-switch clear effect. Zustand action refs are stable in practice, so effect cadence is unchanged, but the deps array is now exhaustive and future-proof against a non-stable refactor. - Body.jsx: expand the short comment above the mirror effects into a block that documents why this component is read-only and lists the escape hatches (fetch fallback here or a loading state in InputPrompt) to reach for if the "explorer mounts before chat drawer" invariant ever breaks. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The prior `schemaMenu?.length` guard on the schema/schema-change useEffect conflated "schemas not loaded yet" (schemaMenu === []) with "loaded but empty" (same []). A reviewer correctly flagged that a project with no DB connection would have the fetch silently suppressed. Fix: - Initialise schemaMenu as null (sentinel for "not yet loaded") instead of []. - Revert the effect guard to `if (schemaMenu)` so it fires the moment schemaMenu transitions from null to an array, regardless of whether that array has items. - Add null-safe guards — `(schemaMenu || [])` — at the JSX consumers (`.map`, `.length` comparisons) that now need to handle the pre-load null state. During loading, seed-related actions remain disabled and the schema dropdown is empty, both the correct UX. - Add an inline comment above the effect explaining the sentinel so this design choice is visible at the call site. Net result: the original optimisation (skip the redundant mount-time fetch while schemaMenu is still []) is preserved, and the reviewer's no-DB regression is fixed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
abhizipstack
left a comment
There was a problem hiding this comment.
Good refactor — the single-writer Zustand store pattern is clean, and the PR description is thorough. Two issues to address before merge (details in inline comments).
The project-switch clear effect was also firing on initial mount (and on any remount of explorer-component within the same session), which briefly wiped valid store data and could leave chat autocomplete empty until the next fetch resolved. Gate the clear with a prevProjectIdRef so clearExplorerData() only fires when projectId actually changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
What
frontend/src/store/explorer-store.jsthat holds the shared responses of bothexplorerSvc.getExplorer(projectId)andexplorerSvc.getDbExplorer(projectId).frontend/src/ide/explorer/explorer-component.jsxto become the single writer for the store — it writes on every successful fetch of either API, and clears both slices on project switch through oneclearExplorerData()call.frontend/src/ide/chat-ai/Body.jsxto stop calling either explorer API directly. It now subscribes toexplorerDataanddbExplorerDatafrom the store and mirrors them into the existingpromptAutoCompleteshape used by chat autocomplete.explorerServiceimport andexplorerSvcref fromBody.jsx(chat-ai body no longer touchesexplorerServiceat all).Why
explorer-component.jsxandBody.jsxwere both independently calling the same two explorer endpoints with the same arguments, causing duplicateGET /explorerandGET /db_explorerrequests every time the chat drawer was opened.@models,@seeds, DB schema/tables) on the other.How
frontend/src/store/explorer-store.js): a minimal Zustand store with two fields —explorerData(=res.data.childrenfrom/explorer, array where[0]=models,[1]=seeds) anddbExplorerData(=res.datafrom/db_explorer, a single DB tree object). SetterssetExplorerData/setDbExplorerData, and a singleclearExplorerDataaction that resets both fields (intentional — both slices belong to the same project and always reset together on project switch). Intentionally nopersistmiddleware — the payloads are refetched on every mount, can be large, and persisting only increases the risk of stale cross-session data.explorer-component.jsx:useExplorerStoreand pulledsetExplorerData,setDbExplorerData,clearExplorerDataalongside the existinguseProjectStoreselectors.getExplorerwrapper, addedsetExplorerData(treeData)right aftersetTreeData(treeData). A single write site covers every refresh trigger — mount, schema switch, model create/rename/delete, run completion, explorer refresh events, etc.getDbEXplorerwrapper, addedsetDbExplorerData(treeData)right aftersetDBExplorer(mappedData). A single write site covers both callers — the[projectId]mount/project-switch effect and the user-triggered refresh debounce.useEffect([projectId])that callsclearExplorerData()so the store doesn't briefly serve the previous project's data toBody.jsxduring a project switch.Body.jsx:useExplorerStoreand subscribed to bothexplorerDataanddbExplorerData.explorerSvc.getExplorer(projectId)andexplorerSvc.getDbExplorer(projectId)fetch blocks entirely, including the entireuseEffectthat housed them (previously gated onisChatDrawerOpen).explorerData[0]/[1]intopromptAutoComplete.modelsData/seedsDataanddbExplorerDataintopromptAutoComplete.dbData. ThepromptAutoCompleteshape consumed downstream byNewChat→InputPromptis unchanged.import { explorerService }, theuseRefimport, and theconst explorerSvc = useRef(explorerService()).currentline —Body.jsxno longer has any direct dependency onexplorerService.Can this PR break any existing features. If yes, please list possible items. If no, please explain why. (PS: Admins do not merge the PR without this section filled)
setTreeData,transformTree,setDBExplorer,mapIconsToTreeData, and all other callers of the internal wrappers inexplorer-component.jsxcontinue to work exactly as before. The only new behavior there is an additional side-effect of writing the raw response into the shared store.promptAutoCompleteshape consumed byNewChat/InputPromptis preserved. The only behavioral change is that models/seeds/DB autocomplete data appears when the explorer has fetched at least once (as part of the IDE's normal boot), rather than being refetched on chat-drawer open. Per the agreed design, the chat drawer is only expected to be opened after the IDE and explorer have mounted, so this is not a regression in practice.clearExplorerDatafiring and the next fetches completing) instead of the previous project's entries. This is intentional and the safer behavior — it prevents cross-project data leakage in the autocomplete list.Database Migrations
Env Config
Relevant Docs
Related Issues or PRs
Dependencies Versions
zustandpackage, consistent withfrontend/src/store/project-store.js,frontend/src/store/model-status-store.js, etc.Notes on Testing
explorer. Reload the page, then open the chat drawer. Expect exactly oneGET /explorerand oneGET /db_explorerrequest on boot (previously each fired twice once the chat drawer was opened).@in the prompt input. Verify that model and seed suggestions still populate identically to before.onRefreshDebounce→getDbEXplorer(projectId, hardReload)).npm run lintinfrontend/— should pass (the 2 pre-existingimport/no-unresolvedwarnings for./ExistingChatand./NewChatinBody.jsxare unrelated to this change). No new unused-import warnings should appear inBody.jsxafter theexplorerService/useRef/explorerSvcremoval.Screenshots
Note: The explorer APIs are not repeated multiple times

Checklist
I have read and understood the Contribution Guidelines.