@@ -3,7 +3,7 @@ import type { EditorChange, EditorConfiguration } from 'codemirror';
3
3
import type { SchemaReference } from 'codemirror-graphql/utils/SchemaReference' ;
4
4
import copyToClipboard from 'copy-to-clipboard' ;
5
5
import { parse , print } from 'graphql' ;
6
- import { useCallback , useEffect , useMemo } from 'react' ;
6
+ import { useCallback , useEffect , useMemo , useRef , useState } from 'react' ;
7
7
8
8
import { useExplorerContext } from '../explorer' ;
9
9
import { usePluginContext } from '../plugin' ;
@@ -388,3 +388,76 @@ export const useHeadersEditorState = (): [
388
388
] => {
389
389
return useEditorState ( 'header' ) ;
390
390
} ;
391
+
392
+ /**
393
+ * Implements an optimistic caching strategy around a useState-like hook in
394
+ * order to prevent loss of updates when the hook has an internal delay and the
395
+ * update function is called again before the updated state is sent out.
396
+ *
397
+ * Use this as a wrapper around `useOperationsEditorState`,
398
+ * `useVariablesEditorState`, or `useHeadersEditorState` if you anticipate
399
+ * calling them with great frequency (due to, for instance, mouse, keyboard, or
400
+ * network events).
401
+ *
402
+ * Example:
403
+ *
404
+ * ```ts
405
+ * const [operationsString, handleEditOperations] =
406
+ * useOptimisticState(useOperationsEditorState());
407
+ * ```
408
+ */
409
+ export function useOptimisticState ( [
410
+ upstreamState ,
411
+ upstreamSetState ,
412
+ ] : ReturnType < typeof useEditorState > ) : ReturnType < typeof useEditorState > {
413
+ const lastStateRef = useRef ( {
414
+ /** The last thing that we sent upstream; we're expecting this back */
415
+ pending : null as string | null ,
416
+ /** The last thing we received from upstream */
417
+ last : upstreamState ,
418
+ } ) ;
419
+
420
+ const [ state , setOperationsText ] = useState ( upstreamState ) ;
421
+
422
+ useEffect ( ( ) => {
423
+ if ( lastStateRef . current . last === upstreamState ) {
424
+ // No change; ignore
425
+ } else {
426
+ lastStateRef . current . last = upstreamState ;
427
+ if ( lastStateRef . current . pending === null ) {
428
+ // Gracefully accept update from upstream
429
+ setOperationsText ( upstreamState ) ;
430
+ } else if ( lastStateRef . current . pending === upstreamState ) {
431
+ // They received our update and sent it back to us - clear pending, and
432
+ // send next if appropriate
433
+ lastStateRef . current . pending = null ;
434
+ if ( upstreamState !== state ) {
435
+ // Change has occurred; upstream it
436
+ lastStateRef . current . pending = state ;
437
+ upstreamSetState ( state ) ;
438
+ }
439
+ } else {
440
+ // They got a different update; overwrite our local state (!!)
441
+ lastStateRef . current . pending = null ;
442
+ setOperationsText ( upstreamState ) ;
443
+ }
444
+ }
445
+ } , [ upstreamState , state , upstreamSetState ] ) ;
446
+
447
+ const setState = useCallback (
448
+ ( newState : string ) => {
449
+ setOperationsText ( newState ) ;
450
+ if (
451
+ lastStateRef . current . pending === null &&
452
+ lastStateRef . current . last !== newState
453
+ ) {
454
+ // No pending updates and change has occurred... send it upstream
455
+ lastStateRef . current . pending = newState ;
456
+ upstreamSetState ( newState ) ;
457
+ }
458
+ } ,
459
+ [ upstreamSetState ] ,
460
+ ) ;
461
+
462
+ return useMemo ( ( ) => [ state , setState ] , [ state , setState ] ) ;
463
+ }
0 commit comments