Skip to content

feat(@graphiql/plugin-doc-explorer): migrate React context to zustand, replace useExplorerContext with useDocExplorer and useDocExplorerActions hooks #3940

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
May 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/famous-eyes-watch.md
Original file line number Diff line number Diff line change
@@ -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

3 changes: 2 additions & 1 deletion packages/graphiql-plugin-doc-explorer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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') {
Expand Down Expand Up @@ -46,9 +50,9 @@ const withErrorSchemaContext: SchemaContextType = {

const DocExplorerWithContext: FC = () => {
return (
<ExplorerContextProvider>
<DocExplorerContextProvider>
<DocExplorer />
</ExplorerContextProvider>
</DocExplorerContextProvider>
);
};

Expand Down Expand Up @@ -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;
};

Expand All @@ -136,9 +140,9 @@ describe('DocExplorer', () => {
schema: initialSchema,
}}
>
<ExplorerContextProvider>
<DocExplorerContextProvider>
<SetInitialStack />
</ExplorerContextProvider>
</DocExplorerContextProvider>
</SchemaContext.Provider>,
);

Expand All @@ -150,9 +154,9 @@ describe('DocExplorer', () => {
schema: initialSchema,
}}
>
<ExplorerContextProvider>
<DocExplorerContextProvider>
<DocExplorer />
</ExplorerContextProvider>
</DocExplorerContextProvider>
</SchemaContext.Provider>,
);

Expand All @@ -167,9 +171,9 @@ describe('DocExplorer', () => {
schema: makeSchema(), // <<< New, but equivalent, schema
}}
>
<ExplorerContextProvider>
<DocExplorerContextProvider>
<DocExplorer />
</ExplorerContextProvider>
</DocExplorerContextProvider>
</SchemaContext.Provider>,
);
const [title2] = container.querySelectorAll('.graphiql-doc-explorer-title');
Expand All @@ -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;
};

Expand All @@ -203,9 +207,9 @@ describe('DocExplorer', () => {
schema: initialSchema,
}}
>
<ExplorerContextProvider>
<DocExplorerContextProvider>
<SetInitialStack />
</ExplorerContextProvider>
</DocExplorerContextProvider>
</SchemaContext.Provider>,
);

Expand All @@ -217,9 +221,9 @@ describe('DocExplorer', () => {
schema: initialSchema,
}}
>
<ExplorerContextProvider>
<DocExplorerContextProvider>
<DocExplorer />
</ExplorerContextProvider>
</DocExplorerContextProvider>
</SchemaContext.Provider>,
);

Expand All @@ -231,16 +235,16 @@ describe('DocExplorer', () => {
<SchemaContext.Provider
value={{
...defaultSchemaContext,
schema: makeSchema('field2'), // <<< New schema with new field name
schema: makeSchema('field2'), // <<< New schema with a new field name
}}
>
<ExplorerContextProvider>
<DocExplorerContextProvider>
<DocExplorer />
</ExplorerContextProvider>
</DocExplorerContextProvider>
</SchemaContext.Provider>,
);
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');
});
});
Original file line number Diff line number Diff line change
@@ -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',
Expand Down Expand Up @@ -54,17 +54,17 @@ const exampleObject = new GraphQLObjectType({
});

const FieldDocumentationWithContext: FC<{
field: ExplorerFieldDef;
field: DocExplorerFieldDef;
}> = props => {
return (
<ExplorerContext.Provider
value={mockExplorerContextValue({
<DocExplorerContext.Provider
value={useMockDocExplorerContextValue({
name: props.field.name,
def: props.field,
})}
>
<FieldDocumentation field={props.field} />
</ExplorerContext.Provider>
</DocExplorerContext.Provider>
);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -28,14 +28,14 @@ const TypeDocumentationWithContext: FC<{ type: GraphQLNamedType }> = props => {
setSchemaReference: null!,
}}
>
<ExplorerContext.Provider
value={mockExplorerContextValue({
<DocExplorerContext.Provider
value={useMockDocExplorerContextValue({
name: unwrapType(props.type).name,
def: props.type,
})}
>
<TypeDocumentation type={props.type} />
</ExplorerContext.Provider>
</DocExplorerContext.Provider>
</SchemaContext.Provider>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<span data-testid="nav-stack">
{JSON.stringify(explorerNavStack[explorerNavStack.length + 1])}
</span>
);
};

const TypeLinkWithContext: typeof TypeLink = props => {
return (
<ExplorerContext.Provider
value={mockExplorerContextValue({
<DocExplorerContext.Provider
value={useMockDocExplorerContextValue({
name: unwrapType(props.type).name,
def: unwrapType(props.type),
})}
>
<TypeLink {...props} />
{/* Print the top of the current nav stack for test assertions */}
<ExplorerContext.Consumer>
{context => (
<span data-testid="nav-stack">
{JSON.stringify(
context!.explorerNavStack[context!.explorerNavStack.length + 1],
)}
</span>
)}
</ExplorerContext.Consumer>
</ExplorerContext.Provider>
<TypeLinkConsumer />
</DocExplorerContext.Provider>
);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -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 => {
Expand All @@ -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<DefaultValueProps> = ({ field }) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -12,7 +12,7 @@ type FieldDocumentationProps = {
/**
* The field or argument that should be rendered.
*/
field: ExplorerFieldDef;
field: DocExplorerFieldDef;
};

export const FieldDocumentation: FC<FieldDocumentationProps> = ({ field }) => {
Expand All @@ -35,7 +35,7 @@ export const FieldDocumentation: FC<FieldDocumentationProps> = ({ field }) => {
);
};

const Arguments: FC<{ field: ExplorerFieldDef }> = ({ field }) => {
const Arguments: FC<{ field: DocExplorerFieldDef }> = ({ field }) => {
const [showDeprecated, setShowDeprecated] = useState(false);
const handleShowDeprecated = () => {
setShowDeprecated(true);
Expand Down Expand Up @@ -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;
Expand Down
Loading