@@ -7,50 +7,68 @@ import "@graphiql/plugin-explorer/style.css";
77import { explorerPlugin } from "@graphiql/plugin-explorer" ;
88import { createGraphiQLFetcher } from "@graphiql/toolkit" ;
99import { GraphiQL , type GraphiQLProps , HISTORY_PLUGIN } from "graphiql" ;
10+ import { useMemo } from "react" ;
1011
1112interface 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