fix(apps/ensadmin): keep open ENSAdmin GraphiQL docs sidebar#2001
fix(apps/ensadmin): keep open ENSAdmin GraphiQL docs sidebar#2001
Conversation
…pen on the omnigraph page. The editor now memoizes its fetcher, storage, and plugins so 1Hz parent re-renders (driven by the realtime indexing-status projection) no longer trigger schema re-introspection
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🦋 Changeset detectedLatest commit: 24f323f The changes in this PR will be included in the next version bump. This PR includes changesets to release 23 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
📝 WalkthroughWalkthroughThis PR optimizes the GraphiQL editor component in the ENSAdmin application by memoizing the fetcher, storage, and plugins to prevent unnecessary re-renders and schema re-introspection during frequent parent updates from the realtime indexing-status projection. Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThis PR memoizes the GraphiQL
Confidence Score: 3/5Not safe to merge as-is — the storage memo accesses localStorage before the SSR guard, causing a ReferenceError during server-side rendering. A P1 bug is present: localStorage.length is evaluated eagerly inside the storage useMemo before the typeof window === 'undefined' guard, which throws in SSR. The fix pattern is small, but it needs to be addressed before merging. apps/ensadmin/src/components/graphiql-editor/components.tsx — the storage useMemo needs a window guard before any localStorage access. Important Files Changed
Reviews (1): Last reviewed commit: "docs(changeset): Fix ENSAdmin GraphiQL d..." | Re-trigger Greptile |
| const storage = useMemo(() => { | ||
| const storageNamespace = `ensnode:graphiql:${url}`; | ||
| return { | ||
| getItem: (key: string) => localStorage.getItem(`${storageNamespace}:${key}`), | ||
| setItem: (key: string, value: string) => | ||
| localStorage.setItem(`${storageNamespace}:${key}`, value), | ||
| removeItem: (key: string) => localStorage.removeItem(`${storageNamespace}:${key}`), | ||
| clear: () => { | ||
| localStorage.clear(); | ||
| }, | ||
| length: localStorage.length, | ||
| }; | ||
| }, [url]); |
There was a problem hiding this comment.
localStorage accessed before SSR guard
The storage memo eagerly evaluates localStorage.length (line 53) and all four methods close over localStorage — both happen before the typeof window === "undefined" guard that was intentionally moved to line 62. In Next.js App Router, "use client" components still SSR on the server, where localStorage is not defined and this throws ReferenceError: localStorage is not defined at render time.
The original code was safe because the const storage = {…} block sat after the early-return guard; hoisting it into a useMemo above the guard removes that protection.
Suggested fix — guard the memo body:
const storage = useMemo(() => {
if (typeof window === "undefined") return null;
const storageNamespace = `ensnode:graphiql:${url}`;
return {
getItem: (key: string) => localStorage.getItem(`${storageNamespace}:${key}`),
setItem: (key: string, value: string) =>
localStorage.setItem(`${storageNamespace}:${key}`, value),
removeItem: (key: string) => localStorage.removeItem(`${storageNamespace}:${key}`),
clear: () => { localStorage.clear(); },
get length() { return localStorage.length; },
};
}, [url]);(Note: the get length() accessor also fixes the stale snapshot of localStorage.length captured at memo-creation time.)
| const mergedPlugins = useMemo( | ||
| () => [HISTORY_PLUGIN, explorerPlugin(), ...plugins], | ||
| [plugins], | ||
| ); |
There was a problem hiding this comment.
explorerPlugin() called inside memo, new instance on every plugins change
explorerPlugin() is called inside the useMemo factory, so a new instance is created whenever plugins changes (e.g. if the caller doesn't stabilize the array reference). In the default case (plugins = EMPTY_PLUGINS) this is fine, but any caller that passes an unstable plugins array will cause the explorer to reset. Extracting the explorer instance to its own memoized value independent of plugins would isolate the two concerns:
const explorer = useMemo(() => explorerPlugin(), []);
const mergedPlugins = useMemo(
() => [HISTORY_PLUGIN, explorer, ...plugins],
[explorer, plugins],
);There was a problem hiding this comment.
Pull request overview
This PR fixes ENSAdmin’s GraphiQL docs sidebar not staying open on the omnigraph page by stabilizing key GraphiQL prop references across frequent parent re-renders, preventing repeated schema re-introspection.
Changes:
- Memoize the GraphiQL
fetcher,storage, andpluginsto keep stable references across renders. - Hoist the default
pluginsvalue to a module-level constant to avoid busting memoization. - Add a changeset documenting the patch release.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| apps/ensadmin/src/components/graphiql-editor/components.tsx | Memoizes fetcher/storage/plugins to prevent GraphiQL schema re-introspection and UI resets on frequent parent renders. |
| .changeset/large-fans-stop.md | Adds a patch changeset describing the GraphiQL docs sidebar fix. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const fetcher = useMemo( | ||
| () => | ||
| createGraphiQLFetcher({ | ||
| url, | ||
| // Disable subscriptions for now since we don't have a WebSocket server | ||
| // legacyWsClient: false, | ||
| subscriptionUrl: undefined, | ||
| wsConnectionParams: undefined, | ||
| }), | ||
| [url], |
There was a problem hiding this comment.
createGraphiQLFetcher is invoked unconditionally via useMemo even when url is falsy. Since the if (!url ...) return null guard is now after the hooks, an empty string URL can still cause fetcher creation (and potentially throw) before the component returns. Consider making the memo callback handle the empty-url case (e.g., return a sentinel fetcher) and then returning null after hooks when there’s no usable fetcher/url.
| return { | ||
| getItem: (key: string) => localStorage.getItem(`${storageNamespace}:${key}`), | ||
| setItem: (key: string, value: string) => | ||
| localStorage.setItem(`${storageNamespace}:${key}`, value), | ||
| removeItem: (key: string) => localStorage.removeItem(`${storageNamespace}:${key}`), | ||
| clear: () => { | ||
| localStorage.clear(); | ||
| }, | ||
| length: localStorage.length, |
There was a problem hiding this comment.
localStorage is accessed during render inside the useMemo callback, but the typeof window === "undefined" guard is evaluated later. In any non-browser render (SSR, node-based tests without jsdom, storybook SSR, etc.), this will throw before the component can return null. Consider guarding inside the memo callback (or providing a no-op/in-memory storage fallback) so the component can safely render when window/localStorage are unavailable.
| return { | |
| getItem: (key: string) => localStorage.getItem(`${storageNamespace}:${key}`), | |
| setItem: (key: string, value: string) => | |
| localStorage.setItem(`${storageNamespace}:${key}`, value), | |
| removeItem: (key: string) => localStorage.removeItem(`${storageNamespace}:${key}`), | |
| clear: () => { | |
| localStorage.clear(); | |
| }, | |
| length: localStorage.length, | |
| if (typeof window === "undefined" || !window.localStorage) { | |
| return { | |
| getItem: (_key: string) => null, | |
| setItem: (_key: string, _value: string) => {}, | |
| removeItem: (_key: string) => {}, | |
| clear: () => {}, | |
| length: 0, | |
| }; | |
| } | |
| return { | |
| getItem: (key: string) => | |
| window.localStorage.getItem(`${storageNamespace}:${key}`), | |
| setItem: (key: string, value: string) => | |
| window.localStorage.setItem(`${storageNamespace}:${key}`, value), | |
| removeItem: (key: string) => | |
| window.localStorage.removeItem(`${storageNamespace}:${key}`), | |
| clear: () => { | |
| window.localStorage.clear(); | |
| }, | |
| length: window.localStorage.length, |
| return { | ||
| getItem: (key: string) => localStorage.getItem(`${storageNamespace}:${key}`), | ||
| setItem: (key: string, value: string) => | ||
| localStorage.setItem(`${storageNamespace}:${key}`, value), | ||
| removeItem: (key: string) => localStorage.removeItem(`${storageNamespace}:${key}`), | ||
| clear: () => { | ||
| localStorage.clear(); |
There was a problem hiding this comment.
storage.clear calls localStorage.clear(), which wipes all localStorage for the origin (including unrelated ENSAdmin state). Since keys are already namespaced, clear should remove only keys within the ensnode:graphiql:${url}:* namespace to avoid surprising data loss.
| return { | |
| getItem: (key: string) => localStorage.getItem(`${storageNamespace}:${key}`), | |
| setItem: (key: string, value: string) => | |
| localStorage.setItem(`${storageNamespace}:${key}`, value), | |
| removeItem: (key: string) => localStorage.removeItem(`${storageNamespace}:${key}`), | |
| clear: () => { | |
| localStorage.clear(); | |
| const storagePrefix = `${storageNamespace}:`; | |
| return { | |
| getItem: (key: string) => localStorage.getItem(`${storageNamespace}:${key}`), | |
| setItem: (key: string, value: string) => | |
| localStorage.setItem(`${storageNamespace}:${key}`, value), | |
| removeItem: (key: string) => localStorage.removeItem(`${storageNamespace}:${key}`), | |
| clear: () => { | |
| const keysToRemove: string[] = []; | |
| for (let index = 0; index < localStorage.length; index += 1) { | |
| const key = localStorage.key(index); | |
| if (key?.startsWith(storagePrefix)) { | |
| keysToRemove.push(key); | |
| } | |
| } | |
| for (const key of keysToRemove) { | |
| localStorage.removeItem(key); | |
| } |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.changeset/large-fans-stop.md:
- Line 5: The changeset's release-note paragraph is missing a terminal period;
open the changeset's release-note paragraph (the sentence beginning "Fix
ENSAdmin GraphiQL docs sidebar...") and add a period at the end so the paragraph
ends with proper punctuation.
In `@apps/ensadmin/src/components/graphiql-editor/components.tsx`:
- Around line 50-52: The current clear() implementation calls
localStorage.clear() which wipes all origin storage; change clear() in
components.tsx to only remove keys that start with the GraphiQL namespace (the
`ensnode:graphiql:${url}` prefix) by iterating localStorage keys and calling
localStorage.removeItem(key) for matches so only per-URL namespaced entries are
deleted; ensure you reference the same `url`/prefix construction used elsewhere
to compute the namespace.
- Around line 43-64: The storage useMemo factory currently accesses
localStorage.length eagerly (in storage = useMemo(...)) which runs during render
and will throw during SSR before the later typeof window guard; fix by moving
the typeof window check into the useMemo factory (or return a safe fallback) and
make length a lazy getter that reads localStorage.length at access time (and
ensure getItem/setItem/removeItem/clear all reference window.localStorage inside
their functions rather than capturing it at memo time). Update the storage
useMemo so it returns a safe no-op storage when window/localStorage is
unavailable or defines length as a getter to avoid SSR crashes and stale
snapshot values.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 21fc13fd-6eab-4768-bad9-45694bf296fc
📒 Files selected for processing (2)
.changeset/large-fans-stop.mdapps/ensadmin/src/components/graphiql-editor/components.tsx
| "ensadmin": patch | ||
| --- | ||
|
|
||
| Fix ENSAdmin GraphiQL docs sidebar failing to stay open on the omnigraph page. The editor now memoizes its fetcher, storage, and plugins so 1Hz parent re-renders (driven by the realtime indexing-status projection) no longer trigger schema re-introspection |
There was a problem hiding this comment.
Nit: missing terminal period.
The release-note paragraph ends mid-sentence without punctuation.
-Fix ENSAdmin GraphiQL docs sidebar failing to stay open on the omnigraph page. The editor now memoizes its fetcher, storage, and plugins so 1Hz parent re-renders (driven by the realtime indexing-status projection) no longer trigger schema re-introspection
+Fix ENSAdmin GraphiQL docs sidebar failing to stay open on the omnigraph page. The editor now memoizes its fetcher, storage, and plugins so 1Hz parent re-renders (driven by the realtime indexing-status projection) no longer trigger schema re-introspection.(MD041 hint ignored as per coding guidelines: "Ignore markdownlint's MD041 flags for changeset files.")
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| Fix ENSAdmin GraphiQL docs sidebar failing to stay open on the omnigraph page. The editor now memoizes its fetcher, storage, and plugins so 1Hz parent re-renders (driven by the realtime indexing-status projection) no longer trigger schema re-introspection | |
| Fix ENSAdmin GraphiQL docs sidebar failing to stay open on the omnigraph page. The editor now memoizes its fetcher, storage, and plugins so 1Hz parent re-renders (driven by the realtime indexing-status projection) no longer trigger schema re-introspection. |
🧰 Tools
🪛 LanguageTool
[grammar] ~5-~5: Ensure spelling is correct
Context: ...es its fetcher, storage, and plugins so 1Hz parent re-renders (driven by the realti...
(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)
[grammar] ~5-~5: Please add a punctuation mark at the end of paragraph.
Context: ...us projection) no longer trigger schema re-introspection
(PUNCTUATION_PARAGRAPH_END)
🪛 markdownlint-cli2 (0.22.1)
[warning] 5-5: First line in a file should be a top-level heading
(MD041, first-line-heading, first-line-h1)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.changeset/large-fans-stop.md at line 5, The changeset's release-note
paragraph is missing a terminal period; open the changeset's release-note
paragraph (the sentence beginning "Fix ENSAdmin GraphiQL docs sidebar...") and
add a period at the end so the paragraph ends with proper punctuation.
| const storage = useMemo(() => { | ||
| const storageNamespace = `ensnode:graphiql:${url}`; | ||
| return { | ||
| getItem: (key: string) => localStorage.getItem(`${storageNamespace}:${key}`), | ||
| setItem: (key: string, value: string) => | ||
| localStorage.setItem(`${storageNamespace}:${key}`, value), | ||
| removeItem: (key: string) => localStorage.removeItem(`${storageNamespace}:${key}`), | ||
| clear: () => { | ||
| localStorage.clear(); | ||
| }, | ||
| length: localStorage.length, | ||
| }; | ||
| }, [url]); | ||
|
|
||
| // Custom storage implementation with namespaced keys | ||
| const storage = { | ||
| getItem: (key: string) => { | ||
| return localStorage.getItem(`${storageNamespace}:${key}`); | ||
| }, | ||
| setItem: (key: string, value: string) => { | ||
| localStorage.setItem(`${storageNamespace}:${key}`, value); | ||
| }, | ||
| removeItem: (key: string) => { | ||
| localStorage.removeItem(`${storageNamespace}:${key}`); | ||
| }, | ||
| clear: () => { | ||
| localStorage.clear(); | ||
| }, | ||
| length: localStorage.length, | ||
| }; | ||
| const mergedPlugins = useMemo( | ||
| () => [HISTORY_PLUGIN, explorerPlugin(), ...plugins], | ||
| [plugins], | ||
| ); | ||
|
|
||
| const explorer = explorerPlugin(); | ||
| if (!url || typeof window === "undefined") { | ||
| return null; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Confirm whether SSR is actually exercised for the omnigraph/subgraph pages
# (i.e. they aren't dynamically imported with { ssr: false }).
fd -t f 'page.tsx' apps/ensadmin/src/app/api | xargs -I{} sh -c 'echo "=== {} ==="; cat "{}"'
rg -nP -C2 "dynamic\s*\(.*GraphiQLEditor|ssr:\s*false" apps/ensadminRepository: namehash/ensnode
Length of output: 3159
🏁 Script executed:
cat -n apps/ensadmin/src/components/graphiql-editor/components.tsx | head -80Repository: namehash/ensnode
Length of output: 2951
Critical: SSR will crash — localStorage.length is read before the typeof window guard.
useMemo runs synchronously during render, so the factory at lines 43–55 executes before the early return at line 62–64. The property length: localStorage.length is eagerly evaluated during object construction (line 53), not lazily. During SSR (these pages have SSR enabled by default; the typeof window guard confirms this is expected), localStorage is undefined and will throw ReferenceError: localStorage is not defined before the guard is reached.
Move the window check inside the memo factory, or use a lazy getter for length:
Suggested fix
const storage = useMemo(() => {
+ if (typeof window === "undefined") return undefined;
const storageNamespace = `ensnode:graphiql:${url}`;
return {
getItem: (key: string) => localStorage.getItem(`${storageNamespace}:${key}`),
setItem: (key: string, value: string) =>
localStorage.setItem(`${storageNamespace}:${key}`, value),
removeItem: (key: string) => localStorage.removeItem(`${storageNamespace}:${key}`),
clear: () => {
localStorage.clear();
},
- length: localStorage.length,
+ get length() {
+ return localStorage.length;
+ },
};
}, [url]);The lazy getter for length also fixes a secondary issue: the current form snapshots the value at memo time, making storage.length stale across writes.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const storage = useMemo(() => { | |
| const storageNamespace = `ensnode:graphiql:${url}`; | |
| return { | |
| getItem: (key: string) => localStorage.getItem(`${storageNamespace}:${key}`), | |
| setItem: (key: string, value: string) => | |
| localStorage.setItem(`${storageNamespace}:${key}`, value), | |
| removeItem: (key: string) => localStorage.removeItem(`${storageNamespace}:${key}`), | |
| clear: () => { | |
| localStorage.clear(); | |
| }, | |
| length: localStorage.length, | |
| }; | |
| }, [url]); | |
| // Custom storage implementation with namespaced keys | |
| const storage = { | |
| getItem: (key: string) => { | |
| return localStorage.getItem(`${storageNamespace}:${key}`); | |
| }, | |
| setItem: (key: string, value: string) => { | |
| localStorage.setItem(`${storageNamespace}:${key}`, value); | |
| }, | |
| removeItem: (key: string) => { | |
| localStorage.removeItem(`${storageNamespace}:${key}`); | |
| }, | |
| clear: () => { | |
| localStorage.clear(); | |
| }, | |
| length: localStorage.length, | |
| }; | |
| const mergedPlugins = useMemo( | |
| () => [HISTORY_PLUGIN, explorerPlugin(), ...plugins], | |
| [plugins], | |
| ); | |
| const explorer = explorerPlugin(); | |
| if (!url || typeof window === "undefined") { | |
| return null; | |
| } | |
| const storage = useMemo(() => { | |
| if (typeof window === "undefined") return undefined; | |
| const storageNamespace = `ensnode:graphiql:${url}`; | |
| return { | |
| getItem: (key: string) => localStorage.getItem(`${storageNamespace}:${key}`), | |
| setItem: (key: string, value: string) => | |
| localStorage.setItem(`${storageNamespace}:${key}`, value), | |
| removeItem: (key: string) => localStorage.removeItem(`${storageNamespace}:${key}`), | |
| clear: () => { | |
| localStorage.clear(); | |
| }, | |
| get length() { | |
| return localStorage.length; | |
| }, | |
| }; | |
| }, [url]); | |
| const mergedPlugins = useMemo( | |
| () => [HISTORY_PLUGIN, explorerPlugin(), ...plugins], | |
| [plugins], | |
| ); | |
| if (!url || typeof window === "undefined") { | |
| return null; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/ensadmin/src/components/graphiql-editor/components.tsx` around lines 43
- 64, The storage useMemo factory currently accesses localStorage.length eagerly
(in storage = useMemo(...)) which runs during render and will throw during SSR
before the later typeof window guard; fix by moving the typeof window check into
the useMemo factory (or return a safe fallback) and make length a lazy getter
that reads localStorage.length at access time (and ensure
getItem/setItem/removeItem/clear all reference window.localStorage inside their
functions rather than capturing it at memo time). Update the storage useMemo so
it returns a safe no-op storage when window/localStorage is unavailable or
defines length as a getter to avoid SSR crashes and stale snapshot values.
| clear: () => { | ||
| localStorage.clear(); | ||
| }, |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
clear() wipes the entire localStorage, defeating the per-URL namespacing.
The whole point of the ensnode:graphiql:${url} prefix is isolation, but clear() calls localStorage.clear() which removes every key in the origin's storage — including keys belonging to other ENSAdmin features and other GraphiQL endpoints. If GraphiQL ever invokes storage.clear() (e.g. via a "clear history" action), users lose unrelated state.
♻️ Restrict `clear()` to keys in this namespace
- clear: () => {
- localStorage.clear();
- },
+ clear: () => {
+ const prefix = `${storageNamespace}:`;
+ for (let i = localStorage.length - 1; i >= 0; i--) {
+ const key = localStorage.key(i);
+ if (key && key.startsWith(prefix)) localStorage.removeItem(key);
+ }
+ },📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| clear: () => { | |
| localStorage.clear(); | |
| }, | |
| clear: () => { | |
| const prefix = `${storageNamespace}:`; | |
| for (let i = localStorage.length - 1; i >= 0; i--) { | |
| const key = localStorage.key(i); | |
| if (key && key.startsWith(prefix)) localStorage.removeItem(key); | |
| } | |
| }, |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/ensadmin/src/components/graphiql-editor/components.tsx` around lines 50
- 52, The current clear() implementation calls localStorage.clear() which wipes
all origin storage; change clear() in components.tsx to only remove keys that
start with the GraphiQL namespace (the `ensnode:graphiql:${url}` prefix) by
iterating localStorage keys and calling localStorage.removeItem(key) for matches
so only per-URL namespaced entries are deleted; ensure you reference the same
`url`/prefix construction used elsewhere to compute the namespace.
| clear: () => { | ||
| localStorage.clear(); | ||
| }, | ||
| length: localStorage.length, |
| const mergedPlugins = useMemo( | ||
| () => [HISTORY_PLUGIN, explorerPlugin(), ...plugins], | ||
| [plugins], |
There was a problem hiding this comment.
| const mergedPlugins = useMemo( | |
| () => [HISTORY_PLUGIN, explorerPlugin(), ...plugins], | |
| [plugins], | |
| // Memoize the explorer plugin so its reference is stable across re-renders. This prevents | |
| // GraphiQL from re-running schema introspection when the plugins array prop changes. | |
| const explorer = useMemo(() => explorerPlugin(), []); | |
| const mergedPlugins = useMemo( | |
| () => [HISTORY_PLUGIN, explorer, ...plugins], | |
| [plugins, explorer], |
The explorerPlugin() function creates a new plugin instance each time the mergedPlugins useMemo re-executes due to plugins prop changes, despite the explorer configuration not changing
…pen on the omnigraph page. The editor now memoizes its fetcher, storage, and plugins so 1Hz parent re-renders (driven by the realtime indexing-status projection) no longer trigger schema re-introspection
Lite PR
Tip: Review docs on the ENSNode PR process
Summary
fetcher,storage, andpluginsinapps/ensadmin/src/components/graphiql-editor/components.tsxso they keep stable references across renders.plugins = []default to a module-level constant so it doesn't bust the memo on every render.if (!url || typeof window === "undefined")early-return below the hooks to keep hook order stable.Why
The omnigraph page's parent ticks once per second via
useNow({ timeToRefresh: 1 })insideuseIndexingStatusWithSwr(driving the realtime indexing-status projection).The
GraphiQLEditorwas rebuilding itsfetcher,storage, and explorer plugin on every render, and GraphiQL re-runs schema introspection whenever thefetcherreference changes. Net result: the docs sidebar would never stay open and the schema was re-fetched every sec.Testing
Notes for Reviewer (Optional)
/api/indexing-statusrefetch and th realtime-projection re-renders are both intentional and unchanged by this PR./api/indexing-statuspolled every 10s, docs sidebar opens and stays open.Pre-Review Checklist (Blocking)