diff --git a/.changeset/shy-parents-bow.md b/.changeset/shy-parents-bow.md new file mode 100644 index 00000000000..90126c595f9 --- /dev/null +++ b/.changeset/shy-parents-bow.md @@ -0,0 +1,5 @@ +--- +'graphiql': minor +--- + +Add a toolbar button for manually triggering introspection diff --git a/.changeset/wild-rings-leave.md b/.changeset/wild-rings-leave.md new file mode 100644 index 00000000000..b751d4e1dd9 --- /dev/null +++ b/.changeset/wild-rings-leave.md @@ -0,0 +1,5 @@ +--- +'@graphiql/react': minor +--- + +Add a method `introspect` to the schema context and provide a short key (`Shift-Ctrl-R`) for triggering introspection diff --git a/packages/graphiql-react/src/schema.tsx b/packages/graphiql-react/src/schema.tsx index af5a0309e20..1ab32b7cefc 100644 --- a/packages/graphiql-react/src/schema.tsx +++ b/packages/graphiql-react/src/schema.tsx @@ -19,6 +19,7 @@ import { Dispatch, ReactNode, SetStateAction, + useCallback, useEffect, useMemo, useRef, @@ -32,14 +33,15 @@ import { createContextHook, createNullableContext } from './utility/context'; * There's a semantic difference between `null` and `undefined`: * - When `null` is passed explicitly as prop, GraphiQL will run schema-less * (i.e. it will never attempt to fetch the schema, even when calling the - * `useFetchSchema` hook). + * `introspect` function). * - When `schema` is `undefined` GraphiQL will attempt to fetch the schema - * when calling `useFetchSchema`. + * when calling `introspect`. */ type MaybeGraphQLSchema = GraphQLSchema | null | undefined; export type SchemaContextType = { fetchError: string | null; + introspect(): void; isFetching: boolean; schema: MaybeGraphQLSchema; setFetchError: Dispatch>; @@ -68,6 +70,12 @@ export function SchemaContextProvider(props: SchemaContextProviderProps) { const [isFetching, setIsFetching] = useState(false); const [fetchError, setFetchError] = useState(null); + /** + * A counter that is incremented each time introspection is triggered or the + * schema state is updated. + */ + const counterRef = useRef(0); + /** * Synchronize prop changes with state */ @@ -79,6 +87,12 @@ export function SchemaContextProvider(props: SchemaContextProviderProps) { ? props.schema : undefined, ); + + /** + * Increment the counter so that in-flight introspection requests don't + * override this change. + */ + counterRef.current++; }, [props.schema]); /** @@ -108,7 +122,7 @@ export function SchemaContextProvider(props: SchemaContextProviderProps) { * Fetch the schema */ const { fetcher, onSchemaChange } = props; - useEffect(() => { + const introspect = useCallback(() => { /** * Only introspect if there is no schema provided via props. If the * prop is passed an introspection result, we do continue but skip the @@ -118,11 +132,12 @@ export function SchemaContextProvider(props: SchemaContextProviderProps) { return; } - let isActive = true; + const counter = ++counterRef.current; + setSchema(undefined); const maybeIntrospectionData = props.schema; - async function introspect() { + async function fetchIntrospectionData() { if (maybeIntrospectionData) { // No need to introspect if we already have the data return maybeIntrospectionData; @@ -193,43 +208,37 @@ export function SchemaContextProvider(props: SchemaContextProviderProps) { setFetchError(responseString); } - introspect() + fetchIntrospectionData() .then(introspectionData => { - // Don't continue if the effect has already been cleaned up - if (!isActive || !introspectionData) { + console.log(counter, counterRef.current); + /** + * Don't continue if another introspection request has been started in + * the meantime or if there is no introspection data. + */ + if (counter !== counterRef.current || !introspectionData) { return; } try { const newSchema = buildClientSchema(introspectionData); - // Only override the schema in state if it's still `undefined` (the - // prop and thus the state could have changed while introspecting, - // so this avoids a race condition by prioritizing the state that - // was set after the introspection request was initialized) - setSchema(current => { - if (current === undefined) { - onSchemaChange?.(newSchema); - return newSchema; - } - return current; - }); + setSchema(newSchema); + onSchemaChange?.(newSchema); } catch (error) { setFetchError(formatError(error as Error)); } }) .catch(error => { - // Don't continue if the effect has already been cleaned up - if (!isActive) { + /** + * Don't continue if another introspection request has been started in + * the meantime. + */ + if (counter !== counterRef.current) { return; } setFetchError(formatError(error)); setIsFetching(false); }); - - return () => { - isActive = false; - }; }, [ fetcher, introspectionQueryName, @@ -239,6 +248,26 @@ export function SchemaContextProvider(props: SchemaContextProviderProps) { props.schema, ]); + /** + * Trigger introspection automatically + */ + useEffect(() => { + introspect(); + }, [introspect]); + + /** + * Trigger introspection manually via short key + */ + useEffect(() => { + function triggerIntrospection(event: KeyboardEvent) { + if (event.keyCode === 82 && event.shiftKey && event.ctrlKey) { + introspect(); + } + } + window.addEventListener('keydown', triggerIntrospection); + return () => window.removeEventListener('keydown', triggerIntrospection); + }); + /** * Derive validation errors from the schema */ @@ -256,13 +285,14 @@ export function SchemaContextProvider(props: SchemaContextProviderProps) { const value = useMemo( () => ({ fetchError, + introspect, isFetching, schema, setFetchError, setSchema, validationErrors, }), - [fetchError, isFetching, schema, validationErrors], + [fetchError, introspect, isFetching, schema, validationErrors], ); return ( diff --git a/packages/graphiql/src/components/DocExplorer/__tests__/TypeDoc.spec.tsx b/packages/graphiql/src/components/DocExplorer/__tests__/TypeDoc.spec.tsx index 0c410b3e3a9..87dff62cbab 100644 --- a/packages/graphiql/src/components/DocExplorer/__tests__/TypeDoc.spec.tsx +++ b/packages/graphiql/src/components/DocExplorer/__tests__/TypeDoc.spec.tsx @@ -28,6 +28,7 @@ function TypeDocWithContext(props: { type: GraphQLNamedType }) { + this.props.schemaContext.introspect()} + title="Fetch GraphQL schema using introspection (Shift-Ctrl-R)" + label="Introspect" + /> {this.props.toolbar?.additionalContent ? this.props.toolbar.additionalContent : null} diff --git a/packages/graphiql/src/components/__tests__/DocExplorer.spec.tsx b/packages/graphiql/src/components/__tests__/DocExplorer.spec.tsx index 9ac04de5e1d..1c3ecc0f93b 100644 --- a/packages/graphiql/src/components/__tests__/DocExplorer.spec.tsx +++ b/packages/graphiql/src/components/__tests__/DocExplorer.spec.tsx @@ -17,6 +17,7 @@ import { ExampleSchema } from './ExampleSchema'; const defaultSchemaContext: SchemaContextType = { fetchError: null, + introspect() {}, isFetching: false, schema: ExampleSchema, setFetchError() {},