Skip to content

Commit 7c1c048

Browse files
notrabshrugs
andauthored
fix(apps/ensadmin): keep open ENSAdmin GraphiQL docs sidebar (#2001)
Co-authored-by: shrugs <mattgcondon@gmail.com>
1 parent 0aa0c5a commit 7c1c048

2 files changed

Lines changed: 54 additions & 31 deletions

File tree

.changeset/large-fans-stop.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"ensadmin": patch
3+
---
4+
5+
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.

apps/ensadmin/src/components/graphiql-editor/components.tsx

Lines changed: 49 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,50 +7,68 @@ import "@graphiql/plugin-explorer/style.css";
77
import { explorerPlugin } from "@graphiql/plugin-explorer";
88
import { createGraphiQLFetcher } from "@graphiql/toolkit";
99
import { GraphiQL, type GraphiQLProps, HISTORY_PLUGIN } from "graphiql";
10+
import { useMemo } from "react";
1011

1112
interface GraphiQLPropsWithUrl extends Omit<GraphiQLProps, "fetcher"> {
1213
/** The URL of the GraphQL endpoint */
1314
url: string;
1415
}
1516

17+
const EMPTY_PLUGINS: NonNullable<GraphiQLProps["plugins"]> = [];
18+
1619
/**
1720
* The GraphiQL editor component used to render the generic GraphiQL editor UI.
1821
* We use this component to render GraphiQL editors.
1922
*/
20-
export function GraphiQLEditor({ url, plugins = [], ...props }: GraphiQLPropsWithUrl) {
21-
if (!url || typeof window === "undefined") {
22-
return null;
23-
}
23+
export function GraphiQLEditor({ url, plugins = EMPTY_PLUGINS, ...props }: GraphiQLPropsWithUrl) {
24+
// Memoize the fetcher so its reference is stable across re-renders. Otherwise
25+
// GraphiQL re-runs schema introspection on every parent re-render (e.g. when
26+
// a parent subscribes to a 1s-ticking hook), which resets the docs sidebar.
27+
const fetcher = useMemo(
28+
() =>
29+
createGraphiQLFetcher({
30+
url,
31+
// Disable subscriptions for now since we don't have a WebSocket server
32+
// legacyWsClient: false,
33+
subscriptionUrl: undefined,
34+
wsConnectionParams: undefined,
35+
}),
36+
[url],
37+
);
2438

25-
const fetcher = createGraphiQLFetcher({
26-
url,
27-
// Disable subscriptions for now since we don't have a WebSocket server
28-
// legacyWsClient: false,
29-
subscriptionUrl: undefined,
30-
wsConnectionParams: undefined,
31-
});
39+
// Guard against SSR: hooks run before the early-return below, and `localStorage`
40+
// is undefined on the server. Returning `undefined` is safe because the component
41+
// returns `null` after the hooks when there's no window.
42+
const storage = useMemo(() => {
43+
if (typeof window === "undefined") return undefined;
44+
const storageNamespace = `ensnode:graphiql:${url}`;
45+
const prefix = `${storageNamespace}:`;
46+
return {
47+
getItem: (key: string) => localStorage.getItem(`${prefix}${key}`),
48+
setItem: (key: string, value: string) => localStorage.setItem(`${prefix}${key}`, value),
49+
removeItem: (key: string) => localStorage.removeItem(`${prefix}${key}`),
50+
// Only clear keys in this namespace so unrelated ENSAdmin state survives.
51+
clear: () => {
52+
for (let i = localStorage.length - 1; i >= 0; i--) {
53+
const key = localStorage.key(i);
54+
if (key?.startsWith(prefix)) localStorage.removeItem(key);
55+
}
56+
},
57+
get length() {
58+
return localStorage.length;
59+
},
60+
};
61+
}, [url]);
3262

33-
// Create a unique storage namespace for each endpoint
34-
const storageNamespace = `ensnode:graphiql:${url}`;
63+
// Instantiated per editor instance — explorerPlugin holds editor-scoped state,
64+
// so sharing one instance across editors causes them to clobber each other.
65+
const explorer = useMemo(() => explorerPlugin(), []);
3566

36-
// Custom storage implementation with namespaced keys
37-
const storage = {
38-
getItem: (key: string) => {
39-
return localStorage.getItem(`${storageNamespace}:${key}`);
40-
},
41-
setItem: (key: string, value: string) => {
42-
localStorage.setItem(`${storageNamespace}:${key}`, value);
43-
},
44-
removeItem: (key: string) => {
45-
localStorage.removeItem(`${storageNamespace}:${key}`);
46-
},
47-
clear: () => {
48-
localStorage.clear();
49-
},
50-
length: localStorage.length,
51-
};
67+
const mergedPlugins = useMemo(() => [HISTORY_PLUGIN, explorer, ...plugins], [explorer, plugins]);
5268

53-
const explorer = explorerPlugin();
69+
if (!url || typeof window === "undefined") {
70+
return null;
71+
}
5472

5573
return (
5674
<div className="flex-1 graphiql-container">
@@ -60,7 +78,7 @@ export function GraphiQLEditor({ url, plugins = [], ...props }: GraphiQLPropsWit
6078
storage={storage}
6179
forcedTheme="light"
6280
fetcher={fetcher}
63-
plugins={[HISTORY_PLUGIN, explorer, ...plugins]}
81+
plugins={mergedPlugins}
6482
{...props}
6583
/>
6684
</div>

0 commit comments

Comments
 (0)