diff --git a/.changeset/famous-eyes-watch.md b/.changeset/famous-eyes-watch.md
new file mode 100644
index 00000000000..02f9c647261
--- /dev/null
+++ b/.changeset/famous-eyes-watch.md
@@ -0,0 +1,7 @@
+---
+'@graphiql/plugin-doc-explorer': minor
+'graphiql': patch
+---
+
+feat(@graphiql/plugin-doc-explorer): migrate React context to zustand, replace `useExplorerContext` with `useDocExplorer` and `useDocExplorerActions` hooks
+
diff --git a/packages/graphiql-plugin-doc-explorer/package.json b/packages/graphiql-plugin-doc-explorer/package.json
index fb353cd62d2..55ecd668dfe 100644
--- a/packages/graphiql-plugin-doc-explorer/package.json
+++ b/packages/graphiql-plugin-doc-explorer/package.json
@@ -43,7 +43,8 @@
"dependencies": {
"react-compiler-runtime": "19.1.0-rc.1",
"@graphiql/react": "^0.32.2",
- "@headlessui/react": "^2.2"
+ "@headlessui/react": "^2.2",
+ "zustand": "^5"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.4.1",
diff --git a/packages/graphiql-plugin-doc-explorer/src/components/__tests__/doc-explorer.spec.tsx b/packages/graphiql-plugin-doc-explorer/src/components/__tests__/doc-explorer.spec.tsx
index 490fb015a6b..9024c50091e 100644
--- a/packages/graphiql-plugin-doc-explorer/src/components/__tests__/doc-explorer.spec.tsx
+++ b/packages/graphiql-plugin-doc-explorer/src/components/__tests__/doc-explorer.spec.tsx
@@ -1,8 +1,12 @@
import { render } from '@testing-library/react';
import { GraphQLInt, GraphQLObjectType, GraphQLSchema } from 'graphql';
-import { FC, useContext, useEffect } from 'react';
+import { FC, useEffect } from 'react';
import { SchemaContext, SchemaContextType } from '@graphiql/react';
-import { ExplorerContext, ExplorerContextProvider } from '../../context';
+import {
+ DocExplorerContextProvider,
+ useDocExplorer,
+ useDocExplorerActions,
+} from '../../context';
import { DocExplorer } from '../doc-explorer';
function makeSchema(fieldName = 'field') {
@@ -46,9 +50,9 @@ const withErrorSchemaContext: SchemaContextType = {
const DocExplorerWithContext: FC = () => {
return (
-
+
-
+
);
};
@@ -117,14 +121,14 @@ describe('DocExplorer', () => {
// A hacky component to set the initial explorer nav stack
const SetInitialStack: React.FC = () => {
- const context = useContext(ExplorerContext)!;
+ const explorerNavStack = useDocExplorer();
+ const { push } = useDocExplorerActions();
useEffect(() => {
- if (context.explorerNavStack.length === 1) {
- context.push({ name: 'Query', def: Query });
- // eslint-disable-next-line unicorn/no-array-push-push -- false positive, push here accept only 1 argument
- context.push({ name: 'field', def: field });
+ if (explorerNavStack.length === 1) {
+ push({ name: 'Query', def: Query });
+ push({ name: 'field', def: field });
}
- }, [context]);
+ }, [explorerNavStack.length, push]);
return null;
};
@@ -136,9 +140,9 @@ describe('DocExplorer', () => {
schema: initialSchema,
}}
>
-
+
-
+
,
);
@@ -150,9 +154,9 @@ describe('DocExplorer', () => {
schema: initialSchema,
}}
>
-
+
-
+
,
);
@@ -167,9 +171,9 @@ describe('DocExplorer', () => {
schema: makeSchema(), // <<< New, but equivalent, schema
}}
>
-
+
-
+
,
);
const [title2] = container.querySelectorAll('.graphiql-doc-explorer-title');
@@ -184,14 +188,14 @@ describe('DocExplorer', () => {
// A hacky component to set the initial explorer nav stack
// eslint-disable-next-line sonarjs/no-identical-functions -- todo: could be refactored
const SetInitialStack: React.FC = () => {
- const context = useContext(ExplorerContext)!;
+ const explorerNavStack = useDocExplorer();
+ const { push } = useDocExplorerActions();
useEffect(() => {
- if (context.explorerNavStack.length === 1) {
- context.push({ name: 'Query', def: Query });
- // eslint-disable-next-line unicorn/no-array-push-push -- false positive, push here accept only 1 argument
- context.push({ name: 'field', def: field });
+ if (explorerNavStack.length === 1) {
+ push({ name: 'Query', def: Query });
+ push({ name: 'field', def: field });
}
- }, [context]);
+ }, [explorerNavStack.length, push]);
return null;
};
@@ -203,9 +207,9 @@ describe('DocExplorer', () => {
schema: initialSchema,
}}
>
-
+
-
+
,
);
@@ -217,9 +221,9 @@ describe('DocExplorer', () => {
schema: initialSchema,
}}
>
-
+
-
+
,
);
@@ -231,16 +235,16 @@ describe('DocExplorer', () => {
-
+
-
+
,
);
const [title2] = container.querySelectorAll('.graphiql-doc-explorer-title');
- // Because `Query.field` doesn't exist any more, the top-most item we can render is `Query`
+ // Because `Query.field` doesn't exist anymore, the top-most item we can render is `Query`
expect(title2.textContent).toEqual('Query');
});
});
diff --git a/packages/graphiql-plugin-doc-explorer/src/components/__tests__/field-documentation.spec.tsx b/packages/graphiql-plugin-doc-explorer/src/components/__tests__/field-documentation.spec.tsx
index 43e941367ed..5b2489baa0b 100644
--- a/packages/graphiql-plugin-doc-explorer/src/components/__tests__/field-documentation.spec.tsx
+++ b/packages/graphiql-plugin-doc-explorer/src/components/__tests__/field-documentation.spec.tsx
@@ -1,9 +1,9 @@
import { FC } from 'react';
import { fireEvent, render } from '@testing-library/react';
import { GraphQLString, GraphQLObjectType, Kind } from 'graphql';
-import { ExplorerContext, ExplorerFieldDef } from '../../context';
+import { DocExplorerContext, DocExplorerFieldDef } from '../../context';
import { FieldDocumentation } from '../field-documentation';
-import { mockExplorerContextValue } from './test-utils';
+import { useMockDocExplorerContextValue } from './test-utils';
const exampleObject = new GraphQLObjectType({
name: 'Query',
@@ -54,17 +54,17 @@ const exampleObject = new GraphQLObjectType({
});
const FieldDocumentationWithContext: FC<{
- field: ExplorerFieldDef;
+ field: DocExplorerFieldDef;
}> = props => {
return (
-
-
+
);
};
diff --git a/packages/graphiql-plugin-doc-explorer/src/components/__tests__/test-utils.ts b/packages/graphiql-plugin-doc-explorer/src/components/__tests__/test-utils.ts
index 7aa29f850b2..a88a767e9d8 100644
--- a/packages/graphiql-plugin-doc-explorer/src/components/__tests__/test-utils.ts
+++ b/packages/graphiql-plugin-doc-explorer/src/components/__tests__/test-utils.ts
@@ -1,18 +1,12 @@
'use no memo';
-
+import { useRef } from 'react';
import { GraphQLNamedType, GraphQLType } from 'graphql';
+import { createDocExplorerStore, DocExplorerNavStackItem } from '../../context';
-import { ExplorerContextType, ExplorerNavStackItem } from '../../context';
-
-export function mockExplorerContextValue(
- navStackItem: ExplorerNavStackItem,
-): ExplorerContextType {
- return {
- explorerNavStack: [navStackItem],
- pop() {},
- push() {},
- reset() {},
- };
+export function useMockDocExplorerContextValue(
+ navStackItem: DocExplorerNavStackItem,
+) {
+ return useRef(createDocExplorerStore(navStackItem));
}
export function unwrapType(type: GraphQLType): GraphQLNamedType {
diff --git a/packages/graphiql-plugin-doc-explorer/src/components/__tests__/type-documentation.spec.tsx b/packages/graphiql-plugin-doc-explorer/src/components/__tests__/type-documentation.spec.tsx
index 72127b9a4ff..eca554d476b 100644
--- a/packages/graphiql-plugin-doc-explorer/src/components/__tests__/type-documentation.spec.tsx
+++ b/packages/graphiql-plugin-doc-explorer/src/components/__tests__/type-documentation.spec.tsx
@@ -11,9 +11,9 @@ import {
GraphQLUnionType,
} from 'graphql';
import { SchemaContext } from '@graphiql/react';
-import { ExplorerContext } from '../../context';
+import { DocExplorerContext } from '../../context';
import { TypeDocumentation } from '../type-documentation';
-import { mockExplorerContextValue, unwrapType } from './test-utils';
+import { useMockDocExplorerContextValue, unwrapType } from './test-utils';
const TypeDocumentationWithContext: FC<{ type: GraphQLNamedType }> = props => {
return (
@@ -28,14 +28,14 @@ const TypeDocumentationWithContext: FC<{ type: GraphQLNamedType }> = props => {
setSchemaReference: null!,
}}
>
-
-
+
);
};
diff --git a/packages/graphiql-plugin-doc-explorer/src/components/__tests__/type-link.spec.tsx b/packages/graphiql-plugin-doc-explorer/src/components/__tests__/type-link.spec.tsx
index f705d985a10..ca757648614 100644
--- a/packages/graphiql-plugin-doc-explorer/src/components/__tests__/type-link.spec.tsx
+++ b/packages/graphiql-plugin-doc-explorer/src/components/__tests__/type-link.spec.tsx
@@ -1,32 +1,34 @@
+import { FC } from 'react';
import { fireEvent, render } from '@testing-library/react';
import { GraphQLNonNull, GraphQLList, GraphQLString } from 'graphql';
-import { ExplorerContext } from '../../context';
+import { DocExplorerContext, useDocExplorer } from '../../context';
import { TypeLink } from '../type-link';
-import { mockExplorerContextValue, unwrapType } from './test-utils';
+import { useMockDocExplorerContextValue, unwrapType } from './test-utils';
const nonNullType = new GraphQLNonNull(GraphQLString);
const listType = new GraphQLList(GraphQLString);
+const TypeLinkConsumer: FC = () => {
+ const explorerNavStack = useDocExplorer();
+ return (
+
+ {JSON.stringify(explorerNavStack[explorerNavStack.length + 1])}
+
+ );
+};
+
const TypeLinkWithContext: typeof TypeLink = props => {
return (
-
{/* Print the top of the current nav stack for test assertions */}
-
- {context => (
-
- {JSON.stringify(
- context!.explorerNavStack[context!.explorerNavStack.length + 1],
- )}
-
- )}
-
-
+
+
);
};
diff --git a/packages/graphiql-plugin-doc-explorer/src/components/default-value.tsx b/packages/graphiql-plugin-doc-explorer/src/components/default-value.tsx
index 34725653721..53f544bd2b0 100644
--- a/packages/graphiql-plugin-doc-explorer/src/components/default-value.tsx
+++ b/packages/graphiql-plugin-doc-explorer/src/components/default-value.tsx
@@ -1,6 +1,6 @@
import { FC } from 'react';
import { astFromValue, print, ValueNode } from 'graphql';
-import { ExplorerFieldDef } from '../context';
+import { DocExplorerFieldDef } from '../context';
import './default-value.css';
const printDefault = (ast?: ValueNode | null): string => {
@@ -14,7 +14,7 @@ type DefaultValueProps = {
/**
* The field or argument for which to render the default value.
*/
- field: ExplorerFieldDef;
+ field: DocExplorerFieldDef;
};
export const DefaultValue: FC = ({ field }) => {
diff --git a/packages/graphiql-plugin-doc-explorer/src/components/doc-explorer.tsx b/packages/graphiql-plugin-doc-explorer/src/components/doc-explorer.tsx
index 1c8c4c3053b..ced3acaff5c 100644
--- a/packages/graphiql-plugin-doc-explorer/src/components/doc-explorer.tsx
+++ b/packages/graphiql-plugin-doc-explorer/src/components/doc-explorer.tsx
@@ -1,7 +1,7 @@
import { isType } from 'graphql';
import { FC, ReactNode } from 'react';
import { ChevronLeftIcon, Spinner, useSchemaContext } from '@graphiql/react';
-import { useExplorerContext } from '../context';
+import { useDocExplorer, useDocExplorerActions } from '../context';
import { FieldDocumentation } from './field-documentation';
import { SchemaDocumentation } from './schema-documentation';
import { Search } from './search';
@@ -12,11 +12,8 @@ export const DocExplorer: FC = () => {
const { fetchError, isFetching, schema, validationErrors } = useSchemaContext(
{ nonNull: true, caller: DocExplorer },
);
- const { explorerNavStack, pop } = useExplorerContext({
- nonNull: true,
- caller: DocExplorer,
- });
-
+ const explorerNavStack = useDocExplorer();
+ const { pop } = useDocExplorerActions();
const navItem = explorerNavStack.at(-1)!;
let content: ReactNode = null;
diff --git a/packages/graphiql-plugin-doc-explorer/src/components/field-documentation.tsx b/packages/graphiql-plugin-doc-explorer/src/components/field-documentation.tsx
index 8cf515a4804..c03d3b02a90 100644
--- a/packages/graphiql-plugin-doc-explorer/src/components/field-documentation.tsx
+++ b/packages/graphiql-plugin-doc-explorer/src/components/field-documentation.tsx
@@ -1,7 +1,7 @@
import { GraphQLArgument } from 'graphql';
import { FC, useState } from 'react';
import { Button, MarkdownContent } from '@graphiql/react';
-import { ExplorerFieldDef } from '../context';
+import { DocExplorerFieldDef } from '../context';
import { Argument } from './argument';
import { DeprecationReason } from './deprecation-reason';
import { Directive } from './directive';
@@ -12,7 +12,7 @@ type FieldDocumentationProps = {
/**
* The field or argument that should be rendered.
*/
- field: ExplorerFieldDef;
+ field: DocExplorerFieldDef;
};
export const FieldDocumentation: FC = ({ field }) => {
@@ -35,7 +35,7 @@ export const FieldDocumentation: FC = ({ field }) => {
);
};
-const Arguments: FC<{ field: ExplorerFieldDef }> = ({ field }) => {
+const Arguments: FC<{ field: DocExplorerFieldDef }> = ({ field }) => {
const [showDeprecated, setShowDeprecated] = useState(false);
const handleShowDeprecated = () => {
setShowDeprecated(true);
@@ -81,7 +81,7 @@ const Arguments: FC<{ field: ExplorerFieldDef }> = ({ field }) => {
);
};
-const Directives: FC<{ field: ExplorerFieldDef }> = ({ field }) => {
+const Directives: FC<{ field: DocExplorerFieldDef }> = ({ field }) => {
const directives = field.astNode?.directives;
if (!directives?.length) {
return null;
diff --git a/packages/graphiql-plugin-doc-explorer/src/components/field-link.tsx b/packages/graphiql-plugin-doc-explorer/src/components/field-link.tsx
index 6079b6c2eed..2cc871fe11b 100644
--- a/packages/graphiql-plugin-doc-explorer/src/components/field-link.tsx
+++ b/packages/graphiql-plugin-doc-explorer/src/components/field-link.tsx
@@ -1,16 +1,16 @@
import { FC } from 'react';
-import { ExplorerFieldDef, useExplorerContext } from '../context';
+import { DocExplorerFieldDef, useDocExplorerActions } from '../context';
import './field-link.css';
type FieldLinkProps = {
/**
* The field or argument that should be linked to.
*/
- field: ExplorerFieldDef;
+ field: DocExplorerFieldDef;
};
export const FieldLink: FC = ({ field }) => {
- const { push } = useExplorerContext({ nonNull: true });
+ const { push } = useDocExplorerActions();
return (
{
- const { explorerNavStack, push } = useExplorerContext({
- nonNull: true,
- caller: Search,
- });
+ const explorerNavStack = useDocExplorer();
+ const { push } = useDocExplorerActions();
const inputRef = useRef(null!);
const getSearchResults = useSearchResults();
@@ -164,10 +160,7 @@ type FieldMatch = {
const _useSearchResults = useSearchResults;
export function useSearchResults(caller?: Function) {
- const { explorerNavStack } = useExplorerContext({
- nonNull: true,
- caller: caller || _useSearchResults,
- });
+ const explorerNavStack = useDocExplorer();
const { schema } = useSchemaContext({
nonNull: true,
caller: caller || _useSearchResults,
diff --git a/packages/graphiql-plugin-doc-explorer/src/components/type-documentation.tsx b/packages/graphiql-plugin-doc-explorer/src/components/type-documentation.tsx
index 10a7ff7bf39..0d43acead4e 100644
--- a/packages/graphiql-plugin-doc-explorer/src/components/type-documentation.tsx
+++ b/packages/graphiql-plugin-doc-explorer/src/components/type-documentation.tsx
@@ -10,7 +10,7 @@ import {
isObjectType,
} from 'graphql';
import { useSchemaContext, Button, MarkdownContent } from '@graphiql/react';
-import { ExplorerFieldDef } from '../context';
+import { DocExplorerFieldDef } from '../context';
import { Argument } from './argument';
import { DefaultValue } from './default-value';
import { DeprecationReason } from './deprecation-reason';
@@ -72,8 +72,8 @@ const Fields: FC<{ type: GraphQLNamedType }> = ({ type }) => {
const fieldMap = type.getFields();
- const fields: ExplorerFieldDef[] = [];
- const deprecatedFields: ExplorerFieldDef[] = [];
+ const fields: DocExplorerFieldDef[] = [];
+ const deprecatedFields: DocExplorerFieldDef[] = [];
for (const field of Object.keys(fieldMap).map(name => fieldMap[name])) {
if (field.deprecationReason) {
@@ -109,7 +109,7 @@ const Fields: FC<{ type: GraphQLNamedType }> = ({ type }) => {
);
};
-const Field: FC<{ field: ExplorerFieldDef }> = ({ field }) => {
+const Field: FC<{ field: DocExplorerFieldDef }> = ({ field }) => {
const args =
'args' in field ? field.args.filter(arg => !arg.deprecationReason) : [];
return (
diff --git a/packages/graphiql-plugin-doc-explorer/src/components/type-link.tsx b/packages/graphiql-plugin-doc-explorer/src/components/type-link.tsx
index b66bf0531e0..a4acdb99741 100644
--- a/packages/graphiql-plugin-doc-explorer/src/components/type-link.tsx
+++ b/packages/graphiql-plugin-doc-explorer/src/components/type-link.tsx
@@ -1,6 +1,6 @@
import { FC } from 'react';
import { GraphQLType } from 'graphql';
-import { useExplorerContext } from '../context';
+import { useDocExplorerActions } from '../context';
import { renderType } from './utils';
import './type-link.css';
@@ -12,7 +12,7 @@ type TypeLinkProps = {
};
export const TypeLink: FC = ({ type }) => {
- const { push } = useExplorerContext({ nonNull: true, caller: TypeLink });
+ const { push } = useDocExplorerActions();
if (!type) {
return null;
diff --git a/packages/graphiql-plugin-doc-explorer/src/context.tsx b/packages/graphiql-plugin-doc-explorer/src/context.tsx
index e9b83510528..022181c344b 100644
--- a/packages/graphiql-plugin-doc-explorer/src/context.tsx
+++ b/packages/graphiql-plugin-doc-explorer/src/context.tsx
@@ -3,6 +3,7 @@ import type {
GraphQLField,
GraphQLInputField,
GraphQLNamedType,
+ GraphQLSchema,
} from 'graphql';
import {
isEnumType,
@@ -13,19 +14,24 @@ import {
isScalarType,
isUnionType,
} from 'graphql';
-import { FC, ReactNode, useEffect, useState } from 'react';
import {
- useSchemaContext,
- createContextHook,
- createNullableContext,
-} from '@graphiql/react';
+ createContext,
+ FC,
+ ReactNode,
+ RefObject,
+ useContext,
+ useEffect,
+ useRef,
+} from 'react';
+import { SchemaContextType, useSchemaContext } from '@graphiql/react';
+import { createStore, StoreApi, useStore } from 'zustand';
-export type ExplorerFieldDef =
+export type DocExplorerFieldDef =
| GraphQLField
| GraphQLInputField
| GraphQLArgument;
-export type ExplorerNavStackItem = {
+export type DocExplorerNavStackItem = {
/**
* The name of the item.
*/
@@ -34,208 +40,259 @@ export type ExplorerNavStackItem = {
* The definition object of the item, this can be a named type, a field, an
* input field or an argument.
*/
- def?: GraphQLNamedType | ExplorerFieldDef;
+ def?: GraphQLNamedType | DocExplorerFieldDef;
};
// There's always at least one item in the nav stack
-export type ExplorerNavStack = [
- ExplorerNavStackItem,
- ...ExplorerNavStackItem[],
+export type DocExplorerNavStack = [
+ DocExplorerNavStackItem,
+ ...DocExplorerNavStackItem[],
];
-const initialNavStackItem: ExplorerNavStackItem = { name: 'Docs' };
-
-export type ExplorerContextType = {
+export type DocExplorerContextType = {
/**
* A stack of navigation items. The last item in the list is the current one.
* This list always contains at least one item.
*/
- explorerNavStack: ExplorerNavStack;
- /**
- * Push an item to the navigation stack.
- * @param item The item that should be pushed to the stack.
- */
- push(item: ExplorerNavStackItem): void;
- /**
- * Pop the last item from the navigation stack.
- */
- pop(): void;
- /**
- * Reset the navigation stack to its initial state, this will remove all but
- * the initial stack item.
- */
- reset(): void;
+ explorerNavStack: DocExplorerNavStack;
+ actions: {
+ /**
+ * Push an item to the navigation stack.
+ * @param item The item that should be pushed to the stack.
+ */
+ push(item: DocExplorerNavStackItem): void;
+ /**
+ * Pop the last item from the navigation stack.
+ */
+ pop(): void;
+ /**
+ * Reset the navigation stack to its initial state, this will remove all but
+ * the initial stack item.
+ */
+ reset(): void;
+ resolveSchemaReferenceToNavItem(
+ schemaReference: SchemaContextType['schemaReference'],
+ ): void;
+ /**
+ * Replace the nav stack with an updated version using the new schema.
+ */
+ rebuildNavStackWithSchema(schema: GraphQLSchema): void;
+ };
};
-export const ExplorerContext =
- createNullableContext('ExplorerContext');
+export function createDocExplorerStore(
+ initialNavStackItem: DocExplorerNavStackItem = { name: 'Docs' },
+) {
+ return createStore((set, get) => ({
+ explorerNavStack: [initialNavStackItem],
+ actions: {
+ push(item) {
+ set(state => {
+ const curr = state.explorerNavStack;
+ const lastItem = curr.at(-1)!;
+ const explorerNavStack: DocExplorerNavStack =
+ // Avoid pushing duplicate items
+ lastItem.def === item.def ? curr : [...curr, item];
+
+ return { explorerNavStack };
+ });
+ },
+ pop() {
+ set(state => {
+ const curr = state.explorerNavStack;
+
+ const explorerNavStack =
+ curr.length > 1 ? (curr.slice(0, -1) as DocExplorerNavStack) : curr;
+
+ return { explorerNavStack };
+ });
+ },
+ reset() {
+ set(state => {
+ const curr = state.explorerNavStack;
+ const explorerNavStack: DocExplorerNavStack =
+ curr.length === 1 ? curr : [initialNavStackItem];
+ return { explorerNavStack };
+ });
+ },
+ resolveSchemaReferenceToNavItem(schemaReference) {
+ if (!schemaReference) {
+ return;
+ }
+ const { push } = get().actions;
+ switch (schemaReference.kind) {
+ case 'Type': {
+ push({
+ name: schemaReference.type.name,
+ def: schemaReference.type,
+ });
+ break;
+ }
+ case 'Field': {
+ push({
+ name: schemaReference.field.name,
+ def: schemaReference.field,
+ });
+ break;
+ }
+ case 'Argument': {
+ if (schemaReference.field) {
+ push({
+ name: schemaReference.field.name,
+ def: schemaReference.field,
+ });
+ }
+ break;
+ }
+ case 'EnumValue': {
+ if (schemaReference.type) {
+ push({
+ name: schemaReference.type.name,
+ def: schemaReference.type,
+ });
+ }
+ break;
+ }
+ }
+ },
+ rebuildNavStackWithSchema(schema: GraphQLSchema) {
+ set(state => {
+ const oldNavStack = state.explorerNavStack;
+ if (oldNavStack.length === 1) {
+ return oldNavStack;
+ }
+ const newNavStack: DocExplorerNavStack = [initialNavStackItem];
+ let lastEntity:
+ | GraphQLNamedType
+ | GraphQLField
+ | null = null;
+ for (const item of oldNavStack) {
+ if (item === initialNavStackItem) {
+ // No need to copy the initial item
+ continue;
+ }
+ if (item.def) {
+ // If item.def isn't a named type, it must be a field, inputField, or argument
+ if (isNamedType(item.def)) {
+ // The type needs to be replaced with the new schema type of the same name
+ const newType = schema.getType(item.def.name);
+ if (newType) {
+ newNavStack.push({
+ name: item.name,
+ def: newType,
+ });
+ lastEntity = newType;
+ } else {
+ // This type no longer exists; the stack cannot be built beyond here
+ break;
+ }
+ } else if (lastEntity === null) {
+ // We can't have a sub-entity if we have no entity; stop rebuilding the nav stack
+ break;
+ } else if (
+ isObjectType(lastEntity) ||
+ isInputObjectType(lastEntity)
+ ) {
+ // item.def must be a Field / input field; replace with the new field of the same name
+ const field = lastEntity.getFields()[item.name];
+ if (field) {
+ newNavStack.push({
+ name: item.name,
+ def: field,
+ });
+ } else {
+ // This field no longer exists; the stack cannot be built beyond here
+ break;
+ }
+ } else if (
+ isScalarType(lastEntity) ||
+ isEnumType(lastEntity) ||
+ isInterfaceType(lastEntity) ||
+ isUnionType(lastEntity)
+ ) {
+ // These don't (currently) have non-type sub-entries; something has gone wrong.
+ // Handle gracefully by discontinuing rebuilding the stack.
+ break;
+ } else {
+ // lastEntity must be a field (because it's not a named type)
+ const field: GraphQLField = lastEntity;
+ // Thus item.def must be an argument, so find the same named argument in the new schema
+ if (field.args.some(a => a.name === item.name)) {
+ newNavStack.push({
+ name: item.name,
+ def: field,
+ });
+ } else {
+ // This argument no longer exists; the stack cannot be built beyond here
+ break;
+ }
+ }
+ } else {
+ lastEntity = null;
+ newNavStack.push(item);
+ }
+ }
+ return { explorerNavStack: newNavStack };
+ });
+ },
+ },
+ }));
+}
+
+export const DocExplorerContext = createContext
+> | null>(null);
-export const ExplorerContextProvider: FC<{
+export const DocExplorerContextProvider: FC<{
children: ReactNode;
}> = props => {
const { schema, validationErrors, schemaReference } = useSchemaContext({
nonNull: true,
- caller: ExplorerContextProvider,
+ caller: DocExplorerContextProvider,
});
- const [navStack, setNavStack] = useState([
- initialNavStackItem,
- ]);
-
- const push = // eslint-disable-line react-hooks/exhaustive-deps -- false positive, variable is optimized by react-compiler, no need to wrap with useCallback
- (item: ExplorerNavStackItem) => {
- setNavStack(currentState => {
- const lastItem = currentState.at(-1)!;
- return lastItem.def === item.def
- ? // Avoid pushing duplicate items
- currentState
- : [...currentState, item];
- });
- };
-
- const pop = () => {
- setNavStack(currentState =>
- currentState.length > 1
- ? (currentState.slice(0, -1) as ExplorerNavStack)
- : currentState,
- );
- };
+ const storeRef = useRef>(null!);
- const reset = () => {
- setNavStack(currentState =>
- currentState.length === 1 ? currentState : [initialNavStackItem],
- );
- };
+ if (storeRef.current === null) {
+ storeRef.current = createDocExplorerStore();
+ }
useEffect(() => {
- if (!schemaReference) {
- return;
- }
- switch (schemaReference.kind) {
- case 'Type': {
- push({ name: schemaReference.type.name, def: schemaReference.type });
- break;
- }
- case 'Field': {
- push({ name: schemaReference.field.name, def: schemaReference.field });
- break;
- }
- case 'Argument': {
- if (schemaReference.field) {
- push({
- name: schemaReference.field.name,
- def: schemaReference.field,
- });
- }
- break;
- }
- case 'EnumValue': {
- if (schemaReference.type) {
- push({ name: schemaReference.type.name, def: schemaReference.type });
- }
- break;
- }
- }
- }, [schemaReference, push]);
+ const { resolveSchemaReferenceToNavItem } =
+ storeRef.current.getState().actions;
+ resolveSchemaReferenceToNavItem(schemaReference);
+ }, [schemaReference]);
useEffect(() => {
+ const { reset, rebuildNavStackWithSchema } =
+ storeRef.current.getState().actions;
+
// Whenever the schema changes, we must revalidate/replace the nav stack.
if (schema == null || validationErrors.length > 0) {
reset();
} else {
- // Replace the nav stack with an updated version using the new schema
- setNavStack(oldNavStack => {
- if (oldNavStack.length === 1) {
- return oldNavStack;
- }
- const newNavStack: ExplorerNavStack = [initialNavStackItem];
- let lastEntity:
- | GraphQLNamedType
- | GraphQLField
- | null = null;
- for (const item of oldNavStack) {
- if (item === initialNavStackItem) {
- // No need to copy the initial item
- continue;
- }
- if (item.def) {
- // If item.def isn't a named type, it must be a field, inputField, or argument
- if (isNamedType(item.def)) {
- // The type needs to be replaced with the new schema type of the same name
- const newType = schema.getType(item.def.name);
- if (newType) {
- newNavStack.push({
- name: item.name,
- def: newType,
- });
- lastEntity = newType;
- } else {
- // This type no longer exists; the stack cannot be built beyond here
- break;
- }
- } else if (lastEntity === null) {
- // We can't have a sub-entity if we have no entity; stop rebuilding the nav stack
- break;
- } else if (
- isObjectType(lastEntity) ||
- isInputObjectType(lastEntity)
- ) {
- // item.def must be a Field / input field; replace with the new field of the same name
- const field = lastEntity.getFields()[item.name];
- if (field) {
- newNavStack.push({
- name: item.name,
- def: field,
- });
- } else {
- // This field no longer exists; the stack cannot be built beyond here
- break;
- }
- } else if (
- isScalarType(lastEntity) ||
- isEnumType(lastEntity) ||
- isInterfaceType(lastEntity) ||
- isUnionType(lastEntity)
- ) {
- // These don't (currently) have non-type sub-entries; something has gone wrong.
- // Handle gracefully by discontinuing rebuilding the stack.
- break;
- } else {
- // lastEntity must be a field (because it's not a named type)
- const field: GraphQLField = lastEntity;
- // Thus item.def must be an argument, so find the same named argument in the new schema
- if (field.args.some(a => a.name === item.name)) {
- newNavStack.push({
- name: item.name,
- def: field,
- });
- } else {
- // This argument no longer exists; the stack cannot be built beyond here
- break;
- }
- }
- } else {
- lastEntity = null;
- newNavStack.push(item);
- }
- }
- return newNavStack;
- });
+ rebuildNavStackWithSchema(schema);
}
}, [schema, validationErrors]);
- const value: ExplorerContextType = {
- explorerNavStack: navStack,
- push,
- pop,
- reset,
- };
-
return (
-
+
{props.children}
-
+
);
};
-export const useExplorerContext = createContextHook(ExplorerContext);
+function useDocExplorerStore(
+ selector: (state: DocExplorerContextType) => T,
+): T {
+ const store = useContext(DocExplorerContext);
+ if (!store) {
+ throw new Error('Missing `DocExplorerContextProvider` in the tree');
+ }
+ return useStore(store.current, selector);
+}
+
+export const useDocExplorer = () =>
+ useDocExplorerStore(state => state.explorerNavStack);
+export const useDocExplorerActions = () =>
+ useDocExplorerStore(state => state.actions);
diff --git a/packages/graphiql-plugin-doc-explorer/src/index.tsx b/packages/graphiql-plugin-doc-explorer/src/index.tsx
index a3913e1e648..2304ea2e672 100644
--- a/packages/graphiql-plugin-doc-explorer/src/index.tsx
+++ b/packages/graphiql-plugin-doc-explorer/src/index.tsx
@@ -9,16 +9,17 @@ import { DocExplorer } from './components';
export * from './components';
export {
- ExplorerContext,
- ExplorerContextProvider,
- useExplorerContext,
+ DocExplorerContext,
+ DocExplorerContextProvider,
+ useDocExplorer,
+ useDocExplorerActions,
} from './context';
export type {
- ExplorerContextType,
- ExplorerFieldDef,
- ExplorerNavStack,
- ExplorerNavStackItem,
+ DocExplorerContextType,
+ DocExplorerFieldDef,
+ DocExplorerNavStack,
+ DocExplorerNavStackItem,
} from './context';
export const DOC_EXPLORER_PLUGIN: GraphiQLPlugin = {
diff --git a/packages/graphiql/src/GraphiQL.tsx b/packages/graphiql/src/GraphiQL.tsx
index bc94754fdb6..ae57914eef9 100644
--- a/packages/graphiql/src/GraphiQL.tsx
+++ b/packages/graphiql/src/GraphiQL.tsx
@@ -60,7 +60,7 @@ import {
HISTORY_PLUGIN,
} from '@graphiql/plugin-history';
import {
- ExplorerContextProvider,
+ DocExplorerContextProvider,
DOC_EXPLORER_PLUGIN,
} from '@graphiql/plugin-doc-explorer';
@@ -156,14 +156,14 @@ const GraphiQL_: FC = ({
return (
-
+
-
+
);