From 2b6ea3166c8d8e152f16d87c878aa8a66f1b3775 Mon Sep 17 00:00:00 2001 From: Benjie Date: Thu, 1 Feb 2024 22:28:48 +0000 Subject: [PATCH] Fix bug in Explorer plugin where characters are dropped when typing quickly (#3526) Use optimistic editor state when interfacing between explorer and GraphiQL --- .changeset/fresh-rabbits-move.md | 5 ++ .changeset/silent-spoons-shout.md | 5 ++ .../graphiql-plugin-explorer/src/index.tsx | 5 +- packages/graphiql-react/src/editor/hooks.ts | 75 ++++++++++++++++++- packages/graphiql-react/src/editor/index.ts | 1 + packages/graphiql-react/src/index.ts | 1 + 6 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 .changeset/fresh-rabbits-move.md create mode 100644 .changeset/silent-spoons-shout.md diff --git a/.changeset/fresh-rabbits-move.md b/.changeset/fresh-rabbits-move.md new file mode 100644 index 00000000000..d123b0ac769 --- /dev/null +++ b/.changeset/fresh-rabbits-move.md @@ -0,0 +1,5 @@ +--- +'@graphiql/plugin-explorer': patch +--- + +Fix bug whereby typing quickly into explorer sidebar would result in characters being dropped. diff --git a/.changeset/silent-spoons-shout.md b/.changeset/silent-spoons-shout.md new file mode 100644 index 00000000000..5c2f1ab9458 --- /dev/null +++ b/.changeset/silent-spoons-shout.md @@ -0,0 +1,5 @@ +--- +'@graphiql/react': patch +--- + +Add new `useOptimisticState` hook that can wrap a useState-like hook to perform optimistic caching of state changes, this helps to avoid losing characters when the user is typing rapidly. Example of usage: `const [state, setState] = useOptimisticState(useOperationsEditorState());` diff --git a/packages/graphiql-plugin-explorer/src/index.tsx b/packages/graphiql-plugin-explorer/src/index.tsx index 01d95386bc2..c7b4bb625fd 100644 --- a/packages/graphiql-plugin-explorer/src/index.tsx +++ b/packages/graphiql-plugin-explorer/src/index.tsx @@ -4,6 +4,7 @@ import { useExecutionContext, useSchemaContext, useOperationsEditorState, + useOptimisticState, } from '@graphiql/react'; import { Explorer as GraphiQLExplorer, @@ -139,7 +140,9 @@ function ExplorerPlugin(props: GraphiQLExplorerPluginProps) { ); // load the current editor tab state into the explorer - const [operationsString, handleEditOperations] = useOperationsEditorState(); + const [operationsString, handleEditOperations] = useOptimisticState( + useOperationsEditorState(), + ); return ( { return useEditorState('header'); }; + +/** + * Implements an optimistic caching strategy around a useState-like hook in + * order to prevent loss of updates when the hook has an internal delay and the + * update function is called again before the updated state is sent out. + * + * Use this as a wrapper around `useOperationsEditorState`, + * `useVariablesEditorState`, or `useHeadersEditorState` if you anticipate + * calling them with great frequency (due to, for instance, mouse, keyboard, or + * network events). + * + * Example: + * + * ```ts + * const [operationsString, handleEditOperations] = + * useOptimisticState(useOperationsEditorState()); + * ``` + */ +export function useOptimisticState([ + upstreamState, + upstreamSetState, +]: ReturnType): ReturnType { + const lastStateRef = useRef({ + /** The last thing that we sent upstream; we're expecting this back */ + pending: null as string | null, + /** The last thing we received from upstream */ + last: upstreamState, + }); + + const [state, setOperationsText] = useState(upstreamState); + + useEffect(() => { + if (lastStateRef.current.last === upstreamState) { + // No change; ignore + } else { + lastStateRef.current.last = upstreamState; + if (lastStateRef.current.pending === null) { + // Gracefully accept update from upstream + setOperationsText(upstreamState); + } else if (lastStateRef.current.pending === upstreamState) { + // They received our update and sent it back to us - clear pending, and + // send next if appropriate + lastStateRef.current.pending = null; + if (upstreamState !== state) { + // Change has occurred; upstream it + lastStateRef.current.pending = state; + upstreamSetState(state); + } + } else { + // They got a different update; overwrite our local state (!!) + lastStateRef.current.pending = null; + setOperationsText(upstreamState); + } + } + }, [upstreamState, state, upstreamSetState]); + + const setState = useCallback( + (newState: string) => { + setOperationsText(newState); + if ( + lastStateRef.current.pending === null && + lastStateRef.current.last !== newState + ) { + // No pending updates and change has occurred... send it upstream + lastStateRef.current.pending = newState; + upstreamSetState(newState); + } + }, + [upstreamSetState], + ); + + return useMemo(() => [state, setState], [state, setState]); +} diff --git a/packages/graphiql-react/src/editor/index.ts b/packages/graphiql-react/src/editor/index.ts index 8d593fcc579..c7a902c4307 100644 --- a/packages/graphiql-react/src/editor/index.ts +++ b/packages/graphiql-react/src/editor/index.ts @@ -18,6 +18,7 @@ export { usePrettifyEditors, useEditorState, useOperationsEditorState, + useOptimisticState, useVariablesEditorState, useHeadersEditorState, } from './hooks'; diff --git a/packages/graphiql-react/src/index.ts b/packages/graphiql-react/src/index.ts index add9f53e364..d52e3bffb24 100644 --- a/packages/graphiql-react/src/index.ts +++ b/packages/graphiql-react/src/index.ts @@ -18,6 +18,7 @@ export { useVariableEditor, useEditorState, useOperationsEditorState, + useOptimisticState, useVariablesEditorState, useHeadersEditorState, VariableEditor,