Skip to content

Commit 5a66864

Browse files
authored
feat(@graphiql/plugin-doc-explorer): migrate React context to zustand, replace useExplorerContext with useDocExplorer and useDocExplorerActions hooks (#3940)
* upd * upd * upd * upd * upd * upd * upd * upd * upd
1 parent 8769e95 commit 5a66864

17 files changed

+344
-288
lines changed

.changeset/famous-eyes-watch.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@graphiql/plugin-doc-explorer': minor
3+
'graphiql': patch
4+
---
5+
6+
feat(@graphiql/plugin-doc-explorer): migrate React context to zustand, replace `useExplorerContext` with `useDocExplorer` and `useDocExplorerActions` hooks
7+

packages/graphiql-plugin-doc-explorer/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@
4343
"dependencies": {
4444
"react-compiler-runtime": "19.1.0-rc.1",
4545
"@graphiql/react": "^0.32.2",
46-
"@headlessui/react": "^2.2"
46+
"@headlessui/react": "^2.2",
47+
"zustand": "^5"
4748
},
4849
"devDependencies": {
4950
"@vitejs/plugin-react": "^4.4.1",

packages/graphiql-plugin-doc-explorer/src/components/__tests__/doc-explorer.spec.tsx

Lines changed: 34 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import { render } from '@testing-library/react';
22
import { GraphQLInt, GraphQLObjectType, GraphQLSchema } from 'graphql';
3-
import { FC, useContext, useEffect } from 'react';
3+
import { FC, useEffect } from 'react';
44
import { SchemaContext, SchemaContextType } from '@graphiql/react';
5-
import { ExplorerContext, ExplorerContextProvider } from '../../context';
5+
import {
6+
DocExplorerContextProvider,
7+
useDocExplorer,
8+
useDocExplorerActions,
9+
} from '../../context';
610
import { DocExplorer } from '../doc-explorer';
711

812
function makeSchema(fieldName = 'field') {
@@ -46,9 +50,9 @@ const withErrorSchemaContext: SchemaContextType = {
4650

4751
const DocExplorerWithContext: FC = () => {
4852
return (
49-
<ExplorerContextProvider>
53+
<DocExplorerContextProvider>
5054
<DocExplorer />
51-
</ExplorerContextProvider>
55+
</DocExplorerContextProvider>
5256
);
5357
};
5458

@@ -117,14 +121,14 @@ describe('DocExplorer', () => {
117121

118122
// A hacky component to set the initial explorer nav stack
119123
const SetInitialStack: React.FC = () => {
120-
const context = useContext(ExplorerContext)!;
124+
const explorerNavStack = useDocExplorer();
125+
const { push } = useDocExplorerActions();
121126
useEffect(() => {
122-
if (context.explorerNavStack.length === 1) {
123-
context.push({ name: 'Query', def: Query });
124-
// eslint-disable-next-line unicorn/no-array-push-push -- false positive, push here accept only 1 argument
125-
context.push({ name: 'field', def: field });
127+
if (explorerNavStack.length === 1) {
128+
push({ name: 'Query', def: Query });
129+
push({ name: 'field', def: field });
126130
}
127-
}, [context]);
131+
}, [explorerNavStack.length, push]);
128132
return null;
129133
};
130134

@@ -136,9 +140,9 @@ describe('DocExplorer', () => {
136140
schema: initialSchema,
137141
}}
138142
>
139-
<ExplorerContextProvider>
143+
<DocExplorerContextProvider>
140144
<SetInitialStack />
141-
</ExplorerContextProvider>
145+
</DocExplorerContextProvider>
142146
</SchemaContext.Provider>,
143147
);
144148

@@ -150,9 +154,9 @@ describe('DocExplorer', () => {
150154
schema: initialSchema,
151155
}}
152156
>
153-
<ExplorerContextProvider>
157+
<DocExplorerContextProvider>
154158
<DocExplorer />
155-
</ExplorerContextProvider>
159+
</DocExplorerContextProvider>
156160
</SchemaContext.Provider>,
157161
);
158162

@@ -167,9 +171,9 @@ describe('DocExplorer', () => {
167171
schema: makeSchema(), // <<< New, but equivalent, schema
168172
}}
169173
>
170-
<ExplorerContextProvider>
174+
<DocExplorerContextProvider>
171175
<DocExplorer />
172-
</ExplorerContextProvider>
176+
</DocExplorerContextProvider>
173177
</SchemaContext.Provider>,
174178
);
175179
const [title2] = container.querySelectorAll('.graphiql-doc-explorer-title');
@@ -184,14 +188,14 @@ describe('DocExplorer', () => {
184188
// A hacky component to set the initial explorer nav stack
185189
// eslint-disable-next-line sonarjs/no-identical-functions -- todo: could be refactored
186190
const SetInitialStack: React.FC = () => {
187-
const context = useContext(ExplorerContext)!;
191+
const explorerNavStack = useDocExplorer();
192+
const { push } = useDocExplorerActions();
188193
useEffect(() => {
189-
if (context.explorerNavStack.length === 1) {
190-
context.push({ name: 'Query', def: Query });
191-
// eslint-disable-next-line unicorn/no-array-push-push -- false positive, push here accept only 1 argument
192-
context.push({ name: 'field', def: field });
194+
if (explorerNavStack.length === 1) {
195+
push({ name: 'Query', def: Query });
196+
push({ name: 'field', def: field });
193197
}
194-
}, [context]);
198+
}, [explorerNavStack.length, push]);
195199
return null;
196200
};
197201

@@ -203,9 +207,9 @@ describe('DocExplorer', () => {
203207
schema: initialSchema,
204208
}}
205209
>
206-
<ExplorerContextProvider>
210+
<DocExplorerContextProvider>
207211
<SetInitialStack />
208-
</ExplorerContextProvider>
212+
</DocExplorerContextProvider>
209213
</SchemaContext.Provider>,
210214
);
211215

@@ -217,9 +221,9 @@ describe('DocExplorer', () => {
217221
schema: initialSchema,
218222
}}
219223
>
220-
<ExplorerContextProvider>
224+
<DocExplorerContextProvider>
221225
<DocExplorer />
222-
</ExplorerContextProvider>
226+
</DocExplorerContextProvider>
223227
</SchemaContext.Provider>,
224228
);
225229

@@ -231,16 +235,16 @@ describe('DocExplorer', () => {
231235
<SchemaContext.Provider
232236
value={{
233237
...defaultSchemaContext,
234-
schema: makeSchema('field2'), // <<< New schema with new field name
238+
schema: makeSchema('field2'), // <<< New schema with a new field name
235239
}}
236240
>
237-
<ExplorerContextProvider>
241+
<DocExplorerContextProvider>
238242
<DocExplorer />
239-
</ExplorerContextProvider>
243+
</DocExplorerContextProvider>
240244
</SchemaContext.Provider>,
241245
);
242246
const [title2] = container.querySelectorAll('.graphiql-doc-explorer-title');
243-
// Because `Query.field` doesn't exist any more, the top-most item we can render is `Query`
247+
// Because `Query.field` doesn't exist anymore, the top-most item we can render is `Query`
244248
expect(title2.textContent).toEqual('Query');
245249
});
246250
});

packages/graphiql-plugin-doc-explorer/src/components/__tests__/field-documentation.spec.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { FC } from 'react';
22
import { fireEvent, render } from '@testing-library/react';
33
import { GraphQLString, GraphQLObjectType, Kind } from 'graphql';
4-
import { ExplorerContext, ExplorerFieldDef } from '../../context';
4+
import { DocExplorerContext, DocExplorerFieldDef } from '../../context';
55
import { FieldDocumentation } from '../field-documentation';
6-
import { mockExplorerContextValue } from './test-utils';
6+
import { useMockDocExplorerContextValue } from './test-utils';
77

88
const exampleObject = new GraphQLObjectType({
99
name: 'Query',
@@ -54,17 +54,17 @@ const exampleObject = new GraphQLObjectType({
5454
});
5555

5656
const FieldDocumentationWithContext: FC<{
57-
field: ExplorerFieldDef;
57+
field: DocExplorerFieldDef;
5858
}> = props => {
5959
return (
60-
<ExplorerContext.Provider
61-
value={mockExplorerContextValue({
60+
<DocExplorerContext.Provider
61+
value={useMockDocExplorerContextValue({
6262
name: props.field.name,
6363
def: props.field,
6464
})}
6565
>
6666
<FieldDocumentation field={props.field} />
67-
</ExplorerContext.Provider>
67+
</DocExplorerContext.Provider>
6868
);
6969
};
7070

packages/graphiql-plugin-doc-explorer/src/components/__tests__/test-utils.ts

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,12 @@
11
'use no memo';
2-
2+
import { useRef } from 'react';
33
import { GraphQLNamedType, GraphQLType } from 'graphql';
4+
import { createDocExplorerStore, DocExplorerNavStackItem } from '../../context';
45

5-
import { ExplorerContextType, ExplorerNavStackItem } from '../../context';
6-
7-
export function mockExplorerContextValue(
8-
navStackItem: ExplorerNavStackItem,
9-
): ExplorerContextType {
10-
return {
11-
explorerNavStack: [navStackItem],
12-
pop() {},
13-
push() {},
14-
reset() {},
15-
};
6+
export function useMockDocExplorerContextValue(
7+
navStackItem: DocExplorerNavStackItem,
8+
) {
9+
return useRef(createDocExplorerStore(navStackItem));
1610
}
1711

1812
export function unwrapType(type: GraphQLType): GraphQLNamedType {

packages/graphiql-plugin-doc-explorer/src/components/__tests__/type-documentation.spec.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ import {
1111
GraphQLUnionType,
1212
} from 'graphql';
1313
import { SchemaContext } from '@graphiql/react';
14-
import { ExplorerContext } from '../../context';
14+
import { DocExplorerContext } from '../../context';
1515
import { TypeDocumentation } from '../type-documentation';
16-
import { mockExplorerContextValue, unwrapType } from './test-utils';
16+
import { useMockDocExplorerContextValue, unwrapType } from './test-utils';
1717

1818
const TypeDocumentationWithContext: FC<{ type: GraphQLNamedType }> = props => {
1919
return (
@@ -28,14 +28,14 @@ const TypeDocumentationWithContext: FC<{ type: GraphQLNamedType }> = props => {
2828
setSchemaReference: null!,
2929
}}
3030
>
31-
<ExplorerContext.Provider
32-
value={mockExplorerContextValue({
31+
<DocExplorerContext.Provider
32+
value={useMockDocExplorerContextValue({
3333
name: unwrapType(props.type).name,
3434
def: props.type,
3535
})}
3636
>
3737
<TypeDocumentation type={props.type} />
38-
</ExplorerContext.Provider>
38+
</DocExplorerContext.Provider>
3939
</SchemaContext.Provider>
4040
);
4141
};

packages/graphiql-plugin-doc-explorer/src/components/__tests__/type-link.spec.tsx

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,34 @@
1+
import { FC } from 'react';
12
import { fireEvent, render } from '@testing-library/react';
23
import { GraphQLNonNull, GraphQLList, GraphQLString } from 'graphql';
3-
import { ExplorerContext } from '../../context';
4+
import { DocExplorerContext, useDocExplorer } from '../../context';
45
import { TypeLink } from '../type-link';
5-
import { mockExplorerContextValue, unwrapType } from './test-utils';
6+
import { useMockDocExplorerContextValue, unwrapType } from './test-utils';
67

78
const nonNullType = new GraphQLNonNull(GraphQLString);
89
const listType = new GraphQLList(GraphQLString);
910

11+
const TypeLinkConsumer: FC = () => {
12+
const explorerNavStack = useDocExplorer();
13+
return (
14+
<span data-testid="nav-stack">
15+
{JSON.stringify(explorerNavStack[explorerNavStack.length + 1])}
16+
</span>
17+
);
18+
};
19+
1020
const TypeLinkWithContext: typeof TypeLink = props => {
1121
return (
12-
<ExplorerContext.Provider
13-
value={mockExplorerContextValue({
22+
<DocExplorerContext.Provider
23+
value={useMockDocExplorerContextValue({
1424
name: unwrapType(props.type).name,
1525
def: unwrapType(props.type),
1626
})}
1727
>
1828
<TypeLink {...props} />
1929
{/* Print the top of the current nav stack for test assertions */}
20-
<ExplorerContext.Consumer>
21-
{context => (
22-
<span data-testid="nav-stack">
23-
{JSON.stringify(
24-
context!.explorerNavStack[context!.explorerNavStack.length + 1],
25-
)}
26-
</span>
27-
)}
28-
</ExplorerContext.Consumer>
29-
</ExplorerContext.Provider>
30+
<TypeLinkConsumer />
31+
</DocExplorerContext.Provider>
3032
);
3133
};
3234

packages/graphiql-plugin-doc-explorer/src/components/default-value.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { FC } from 'react';
22
import { astFromValue, print, ValueNode } from 'graphql';
3-
import { ExplorerFieldDef } from '../context';
3+
import { DocExplorerFieldDef } from '../context';
44
import './default-value.css';
55

66
const printDefault = (ast?: ValueNode | null): string => {
@@ -14,7 +14,7 @@ type DefaultValueProps = {
1414
/**
1515
* The field or argument for which to render the default value.
1616
*/
17-
field: ExplorerFieldDef;
17+
field: DocExplorerFieldDef;
1818
};
1919

2020
export const DefaultValue: FC<DefaultValueProps> = ({ field }) => {

packages/graphiql-plugin-doc-explorer/src/components/doc-explorer.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { isType } from 'graphql';
22
import { FC, ReactNode } from 'react';
33
import { ChevronLeftIcon, Spinner, useSchemaContext } from '@graphiql/react';
4-
import { useExplorerContext } from '../context';
4+
import { useDocExplorer, useDocExplorerActions } from '../context';
55
import { FieldDocumentation } from './field-documentation';
66
import { SchemaDocumentation } from './schema-documentation';
77
import { Search } from './search';
@@ -12,11 +12,8 @@ export const DocExplorer: FC = () => {
1212
const { fetchError, isFetching, schema, validationErrors } = useSchemaContext(
1313
{ nonNull: true, caller: DocExplorer },
1414
);
15-
const { explorerNavStack, pop } = useExplorerContext({
16-
nonNull: true,
17-
caller: DocExplorer,
18-
});
19-
15+
const explorerNavStack = useDocExplorer();
16+
const { pop } = useDocExplorerActions();
2017
const navItem = explorerNavStack.at(-1)!;
2118

2219
let content: ReactNode = null;

packages/graphiql-plugin-doc-explorer/src/components/field-documentation.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { GraphQLArgument } from 'graphql';
22
import { FC, useState } from 'react';
33
import { Button, MarkdownContent } from '@graphiql/react';
4-
import { ExplorerFieldDef } from '../context';
4+
import { DocExplorerFieldDef } from '../context';
55
import { Argument } from './argument';
66
import { DeprecationReason } from './deprecation-reason';
77
import { Directive } from './directive';
@@ -12,7 +12,7 @@ type FieldDocumentationProps = {
1212
/**
1313
* The field or argument that should be rendered.
1414
*/
15-
field: ExplorerFieldDef;
15+
field: DocExplorerFieldDef;
1616
};
1717

1818
export const FieldDocumentation: FC<FieldDocumentationProps> = ({ field }) => {
@@ -35,7 +35,7 @@ export const FieldDocumentation: FC<FieldDocumentationProps> = ({ field }) => {
3535
);
3636
};
3737

38-
const Arguments: FC<{ field: ExplorerFieldDef }> = ({ field }) => {
38+
const Arguments: FC<{ field: DocExplorerFieldDef }> = ({ field }) => {
3939
const [showDeprecated, setShowDeprecated] = useState(false);
4040
const handleShowDeprecated = () => {
4141
setShowDeprecated(true);
@@ -81,7 +81,7 @@ const Arguments: FC<{ field: ExplorerFieldDef }> = ({ field }) => {
8181
);
8282
};
8383

84-
const Directives: FC<{ field: ExplorerFieldDef }> = ({ field }) => {
84+
const Directives: FC<{ field: DocExplorerFieldDef }> = ({ field }) => {
8585
const directives = field.astNode?.directives;
8686
if (!directives?.length) {
8787
return null;

0 commit comments

Comments
 (0)