Skip to content

Commit

Permalink
add SchemaContext to @graphiql/react (#2420)
Browse files Browse the repository at this point in the history
* set initial value for editors

* make editor context non-null

* remove unused prop

* add schema context

* consume schema context in doc explorer

* stop fetching and validating schema in GraphiQL component

* avoid passing schema as prop to query editor

* replace schema from state with schema from context

* move auto-complete logic to context

* fix tests

* adjust changesets
  • Loading branch information
thomasheyenbrock authored May 23, 2022
1 parent 84d8985 commit 3467cd3
Show file tree
Hide file tree
Showing 19 changed files with 707 additions and 428 deletions.
5 changes: 5 additions & 0 deletions .changeset/spicy-moose-look.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'graphiql': patch
---

Fix sending multiple introspection requests when loading the page
5 changes: 5 additions & 0 deletions .changeset/three-bugs-develop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'graphiql': patch
---

Deprecate the `autoCompleteLeafs` method of the `GraphiQL` component in favor of the function provided by the `EditorContext` from `@graphiql/react`
6 changes: 6 additions & 0 deletions .changeset/unlucky-chefs-hope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'graphiql': patch
'@graphiql/react': minor
---

Add a `SchemaContext` to `@graphiql/react` that replaces the logic for fetching and validating the schema in the `graphiql` package
101 changes: 86 additions & 15 deletions packages/graphiql-react/src/editor/context.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import { createContext, ReactNode, useState } from 'react';
import { fillLeafs, GetDefaultFieldNamesFn } from '@graphiql/toolkit';
import {
createContext,
ReactNode,
useCallback,
useMemo,
useState,
} from 'react';
import { useSchemaWithError } from '../schema';

import { CodeMirrorEditor } from './types';

export type EditorContextType = {
autoCompleteLeafs(): string | undefined;
headerEditor: CodeMirrorEditor | null;
queryEditor: CodeMirrorEditor | null;
responseEditor: CodeMirrorEditor | null;
Expand All @@ -14,6 +23,9 @@ export type EditorContextType = {
};

export const EditorContext = createContext<EditorContextType>({
autoCompleteLeafs() {
return undefined;
},
headerEditor: null,
queryEditor: null,
responseEditor: null,
Expand All @@ -24,10 +36,13 @@ export const EditorContext = createContext<EditorContextType>({
setVariableEditor() {},
});

export function EditorContextProvider(props: {
type EditorContextProviderProps = {
children: ReactNode;
initialValue?: string;
}) {
getDefaultFieldNames?: GetDefaultFieldNamesFn;
};

export function EditorContextProvider(props: EditorContextProviderProps) {
const { schema } = useSchemaWithError('component', 'EditorContextProvider');
const [headerEditor, setHeaderEditor] = useState<CodeMirrorEditor | null>(
null,
);
Expand All @@ -38,18 +53,74 @@ export function EditorContextProvider(props: {
const [variableEditor, setVariableEditor] = useState<CodeMirrorEditor | null>(
null,
);

const autoCompleteLeafs = useCallback<
EditorContextType['autoCompleteLeafs']
>(() => {
if (!queryEditor) {
return;
}

const query = queryEditor.getValue();
const { insertions, result } = fillLeafs(
schema,
query,
props.getDefaultFieldNames,
);
if (insertions && insertions.length > 0) {
queryEditor.operation(() => {
const cursor = queryEditor.getCursor();
const cursorIndex = queryEditor.indexFromPos(cursor);
queryEditor.setValue(result || '');
let added = 0;
const markers = insertions.map(({ index, string }) =>
queryEditor.markText(
queryEditor.posFromIndex(index + added),
queryEditor.posFromIndex(index + (added += string.length)),
{
className: 'autoInsertedLeaf',
clearOnEnter: true,
title: 'Automatically added leaf fields',
},
),
);
setTimeout(() => markers.forEach(marker => marker.clear()), 7000);
let newCursorIndex = cursorIndex;
insertions.forEach(({ index, string }) => {
if (index < cursorIndex) {
newCursorIndex += string.length;
}
});
queryEditor.setCursor(queryEditor.posFromIndex(newCursorIndex));
});
}

return result;
}, [props.getDefaultFieldNames, queryEditor, schema]);

const value = useMemo<EditorContextType>(
() => ({
autoCompleteLeafs,
headerEditor,
queryEditor,
responseEditor,
variableEditor,
setHeaderEditor,
setQueryEditor,
setResponseEditor,
setVariableEditor,
}),
[
autoCompleteLeafs,
headerEditor,
queryEditor,
responseEditor,
variableEditor,
],
);

return (
<EditorContext.Provider
value={{
headerEditor,
queryEditor,
responseEditor,
variableEditor,
setHeaderEditor,
setQueryEditor,
setResponseEditor,
setVariableEditor,
}}>
<EditorContext.Provider value={value}>
{props.children}
</EditorContext.Provider>
);
Expand Down
3 changes: 3 additions & 0 deletions packages/graphiql-react/src/editor/header-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ export function useHeaderEditor({

const { headerEditor, setHeaderEditor } = context;

const initialValue = useRef(value);

useEffect(() => {
let isActive = true;

Expand All @@ -63,6 +65,7 @@ export function useHeaderEditor({
}

const newEditor = CodeMirror(container, {
value: initialValue.current || '',
lineNumbers: true,
tabSize: 2,
mode: { name: 'javascript', json: true },
Expand Down
6 changes: 2 additions & 4 deletions packages/graphiql-react/src/editor/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,8 @@ export function useSynchronizeValue(
value: string | undefined,
) {
useEffect(() => {
if (editor && typeof value !== 'undefined') {
if (value !== editor.getValue()) {
editor.setValue(value);
}
if (editor && typeof value !== 'undefined' && value !== editor.getValue()) {
editor.setValue(value);
}
}, [editor, value]);
}
Expand Down
13 changes: 8 additions & 5 deletions packages/graphiql-react/src/editor/query-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
import { markdown } from '../markdown';
import { normalizeWhitespace } from './whitespace';
import { CodeMirrorType, CodeMirrorEditor } from './types';
import { useSchemaWithError } from '../schema';

type OnClickReference = (reference: SchemaReference) => void;

Expand All @@ -35,7 +36,6 @@ export type UseQueryEditorArgs = {
onMergeQuery?: EmptyCallback;
onRunQuery?: EmptyCallback;
readOnly?: boolean;
schema?: GraphQLSchema | null;
validationRules?: ValidationRule[];
value?: string;
};
Expand All @@ -51,27 +51,29 @@ export function useQueryEditor({
onPrettifyQuery,
onRunQuery,
readOnly = false,
schema,
validationRules,
value,
}: UseQueryEditorArgs = {}) {
const context = useContext(EditorContext);
const { schema } = useSchemaWithError('hook', 'useQueryEditor');
const editorContext = useContext(EditorContext);
const ref = useRef<HTMLDivElement>(null);
const codeMirrorRef = useRef<CodeMirrorType>();

if (!context) {
if (!editorContext) {
throw new Error(
'Tried to call the `useQueryEditor` hook without the necessary context. Make sure that the `EditorContextProvider` from `@graphiql/react` is rendered higher in the tree.',
);
}

const { queryEditor, setQueryEditor } = context;
const { queryEditor, setQueryEditor } = editorContext;

const onClickReferenceRef = useRef<OnClickReference>();
useEffect(() => {
onClickReferenceRef.current = onClickReference;
}, [onClickReference]);

const initialValue = useRef(value);

useEffect(() => {
let isActive = true;

Expand All @@ -97,6 +99,7 @@ export function useQueryEditor({
}

const newEditor = CodeMirror(container, {
value: initialValue.current || '',
lineNumbers: true,
tabSize: 2,
foldGutter: true,
Expand Down
24 changes: 21 additions & 3 deletions packages/graphiql-react/src/editor/response-editor.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { formatError } from '@graphiql/toolkit';
import type { Position, Token } from 'codemirror';
import { ComponentType, useContext, useEffect, useRef } from 'react';
import ReactDOM from 'react-dom';
Expand All @@ -7,6 +8,7 @@ import { ImagePreview } from './components';
import { EditorContext } from './context';
import { useResizeEditor, useSynchronizeValue } from './hooks';
import { CodeMirrorEditor } from './types';
import { useSchemaWithError } from '../schema';

export type ResponseTooltipType = ComponentType<{ pos: Position }>;

Expand All @@ -21,7 +23,11 @@ export function useResponseEditor({
editorTheme = 'graphiql',
value,
}: UseResponseEditorArgs = {}) {
const context = useContext(EditorContext);
const { fetchError, validationErrors } = useSchemaWithError(
'hook',
'useResponseEditor',
);
const editorContext = useContext(EditorContext);
const ref = useRef<HTMLDivElement>(null);

const responseTooltipRef = useRef<ResponseTooltipType | undefined>(
Expand All @@ -31,13 +37,15 @@ export function useResponseEditor({
responseTooltipRef.current = ResponseTooltip;
}, [ResponseTooltip]);

if (!context) {
if (!editorContext) {
throw new Error(
'Tried to call the `useResponseEditor` hook without the necessary context. Make sure that the `EditorContextProvider` from `@graphiql/react` is rendered higher in the tree.',
);
}

const { responseEditor, setResponseEditor } = context;
const { responseEditor, setResponseEditor } = editorContext;

const initialValue = useRef(value);

useEffect(() => {
let isActive = true;
Expand Down Expand Up @@ -95,6 +103,7 @@ export function useResponseEditor({
}

const newEditor = CodeMirror(container, {
value: initialValue.current || '',
lineWrapping: true,
readOnly: true,
theme: editorTheme,
Expand All @@ -119,5 +128,14 @@ export function useResponseEditor({

useResizeEditor(responseEditor, ref);

useEffect(() => {
if (fetchError) {
responseEditor?.setValue(fetchError);
}
if (validationErrors) {
responseEditor?.setValue(formatError(validationErrors));
}
}, [responseEditor, fetchError, validationErrors]);

return ref;
}
4 changes: 3 additions & 1 deletion packages/graphiql-react/src/editor/variable-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ export function useVariableEditor({

const { variableEditor, setVariableEditor } = context;

const initialValue = useRef(value);

useEffect(() => {
let isActive = true;

Expand All @@ -71,7 +73,7 @@ export function useVariableEditor({
}

const newEditor = CodeMirror(container, {
value: '',
value: initialValue.current || '',
lineNumbers: true,
tabSize: 2,
mode: 'graphql-variables',
Expand Down
22 changes: 20 additions & 2 deletions packages/graphiql-react/src/explorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ import {
ReactNode,
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from 'react';
import { useSchemaWithError } from './schema';

export type ExplorerFieldDef =
| GraphQLField<{}, {}, {}>
Expand Down Expand Up @@ -46,6 +49,11 @@ export type ExplorerContextType = {
export const ExplorerContext = createContext<ExplorerContextType | null>(null);

export function ExplorerContextProvider(props: { children: ReactNode }) {
const { isFetching } = useSchemaWithError(
'component',
'ExplorerContextProvider',
);

const [state, setState] = useState<ExplorerNavStack>([initialNavStackItem]);

const push = useCallback((item: ExplorerNavStackItem) => {
Expand Down Expand Up @@ -80,9 +88,19 @@ export function ExplorerContextProvider(props: { children: ReactNode }) {
});
}, []);

useEffect(() => {
if (isFetching) {
reset();
}
}, [isFetching, reset]);

const value = useMemo<ExplorerContextType>(
() => ({ explorerNavStack: state, push, pop, reset, showSearch }),
[state, push, pop, reset, showSearch],
);

return (
<ExplorerContext.Provider
value={{ explorerNavStack: state, push, pop, reset, showSearch }}>
<ExplorerContext.Provider value={value}>
{props.children}
</ExplorerContext.Provider>
);
Expand Down
Loading

0 comments on commit 3467cd3

Please sign in to comment.