Skip to content
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