Skip to content

Commit 4fb6deb

Browse files
committed
fix race conditions related to introspection prop
Fixes #336
1 parent 84d4000 commit 4fb6deb

File tree

2 files changed

+70
-14
lines changed

2 files changed

+70
-14
lines changed

src/components/Voyager.tsx

+16-14
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919

2020
import { getTypeGraph } from '../graph/';
2121
import { extractTypeName, getSchema, typeNameToId } from '../introspection';
22+
import { MaybePromise, usePromise } from '../utils/usePromise';
2223
import DocExplorer from './doc-explorer/DocExplorer';
2324
import GraphViewport from './GraphViewport';
2425
import { IntrospectionModal } from './IntrospectionModal';
@@ -37,7 +38,9 @@ export interface VoyagerDisplayOptions {
3738
}
3839

3940
export interface VoyagerProps {
40-
introspection: Promise<ExecutionResult<IntrospectionQuery> | GraphQLSchema>;
41+
introspection?: MaybePromise<
42+
ExecutionResult<IntrospectionQuery> | GraphQLSchema
43+
>;
4144
displayOptions?: VoyagerDisplayOptions;
4245
introspectionPresets?: { [name: string]: any };
4346
allowToChangeSchema?: boolean;
@@ -62,29 +65,28 @@ export default function Voyager(props: VoyagerProps) {
6265
[props.displayOptions],
6366
);
6467

65-
const [introspectionModalOpen, setIntrospectionModalOpen] = useState(false);
66-
const [introspectionResult, setIntrospectionResult] = useState(null);
68+
const [introspectionModalOpen, setIntrospectionModalOpen] = useState(
69+
props.introspection == null,
70+
);
71+
const [introspectionResult, resolveIntrospectionResult] = usePromise(
72+
props.introspection,
73+
);
6774
const [displayOptions, setDisplayOptions] = useState(initialDisplayOptions);
6875

69-
useEffect(() => {
70-
// FIXME: handle rejection and also handle errors inside introspection
71-
// eslint-disable-next-line @typescript-eslint/no-floating-promises
72-
Promise.resolve(props.introspection).then(setIntrospectionResult);
73-
}, [props.introspection]);
74-
7576
useEffect(() => {
7677
setDisplayOptions(initialDisplayOptions);
7778
}, [introspectionResult, initialDisplayOptions]);
7879

7980
const typeGraph = useMemo(() => {
80-
if (introspectionResult == null) {
81+
if (introspectionResult.loading || introspectionResult.value == null) {
82+
// FIXME: display introspectionResult.error
8183
return null;
8284
}
8385

8486
const introspectionSchema =
85-
introspectionResult instanceof GraphQLSchema
86-
? introspectionResult
87-
: buildClientSchema(introspectionResult.data);
87+
introspectionResult.value instanceof GraphQLSchema
88+
? introspectionResult.value
89+
: buildClientSchema(introspectionResult.value.data);
8890

8991
const schema = getSchema(
9092
introspectionSchema,
@@ -133,7 +135,7 @@ export default function Voyager(props: VoyagerProps) {
133135
open={introspectionModalOpen}
134136
presets={props.introspectionPresets}
135137
onClose={() => setIntrospectionModalOpen(false)}
136-
onChange={setIntrospectionResult}
138+
onChange={resolveIntrospectionResult}
137139
/>
138140
);
139141
}

src/utils/usePromise.ts

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { useCallback, useEffect, useState } from 'react';
2+
3+
type PromiseState<T> =
4+
| { loading: true; error: null; value: undefined }
5+
| { loading: false; error: null; value: T }
6+
| { loading: false; error: unknown; value: undefined };
7+
8+
export type MaybePromise<T> = Promise<T> | T;
9+
10+
export function usePromise<T>(
11+
maybePromise: MaybePromise<T>,
12+
): [PromiseState<T>, (value: T) => void] {
13+
const [state, setState] = useState<PromiseState<T>>({
14+
loading: true,
15+
error: null,
16+
value: undefined,
17+
});
18+
19+
useEffect(() => {
20+
let isMounted = true;
21+
22+
setState({ loading: true, error: null, value: undefined });
23+
Promise.resolve(maybePromise).then(
24+
(value) => {
25+
if (isMounted) {
26+
setState((oldState) =>
27+
oldState.loading // update only if unresolved
28+
? { loading: false, error: null, value }
29+
: oldState,
30+
);
31+
}
32+
},
33+
(error) => {
34+
if (isMounted) {
35+
setState((oldState) =>
36+
oldState.loading // update only if unresolved
37+
? { loading: false, error, value: undefined }
38+
: oldState,
39+
);
40+
}
41+
},
42+
);
43+
44+
return () => {
45+
isMounted = false;
46+
};
47+
}, [maybePromise]);
48+
49+
const resolveValue = useCallback((value: T) => {
50+
setState({ loading: false, error: null, value });
51+
}, []);
52+
53+
return [state, resolveValue];
54+
}

0 commit comments

Comments
 (0)