From 6dd7bf127730d697801617bbebad0e4064a034df Mon Sep 17 00:00:00 2001 From: Brian Kim Date: Fri, 8 Apr 2022 12:29:50 -0700 Subject: [PATCH] TEMP COMMIT DO NOT MERGE --- src/react/hooks/useQuery.ts | 161 +++++++++++++++++------------------- 1 file changed, 75 insertions(+), 86 deletions(-) diff --git a/src/react/hooks/useQuery.ts b/src/react/hooks/useQuery.ts index 20a62cbe9eb..24fe95eadda 100644 --- a/src/react/hooks/useQuery.ts +++ b/src/react/hooks/useQuery.ts @@ -23,6 +23,7 @@ import { import { DocumentType, verifyDocumentType } from '../parser'; import { useApolloClient } from './useApolloClient'; import { canUseWeakMap, isNonEmptyArray } from '../../utilities'; +import { useSyncExternalStore } from 'use-sync-external-store/shim'; const { prototype: { @@ -46,7 +47,7 @@ export function useQuery< export function useInternalState( client: ApolloClient, query: DocumentNode | TypedDocumentNode, -) { +): InternalState { const stateRef = useRef>(); if ( !stateRef.current || @@ -95,18 +96,84 @@ class InternalState { // happening), but that's fine as long as it has been initialized that way, // rather than left uninitialized. this.renderPromises = useContext(getApolloContext()).renderPromises; - this.useOptions(options); - const obsQuery = this.useObservableQuery(); - this.useSubscriptionEffect(obsQuery); + return useSyncExternalStore( + (onStoreChange) => { + if (this.renderPromises) { + // The subscribe callback always expects a callback to be returned. + return () => {}; + } - const result = this.getCurrentResult(); + const onNext = () => { + const previousResult = this.result; + // We use `getCurrentResult()` instead of the onNext argument because + // the values differ slightly. Specifically, loading results will have + // an empty object for data instead of `undefined` for some reason. + const result = obsQuery.getCurrentResult(); + // Make sure we're not attempting to re-render similar results + if ( + previousResult && + previousResult.loading === result.loading && + previousResult.networkStatus === result.networkStatus && + equal(previousResult.data, result.data) + ) { + return; + } + + this.setResult(result); + }; + + const onError = (error: Error) => { + const last = obsQuery["last"]; + subscription.unsubscribe(); + // Unfortunately, if `lastError` is set in the current + // `observableQuery` when the subscription is re-created, + // the subscription will immediately receive the error, which will + // cause it to terminate again. To avoid this, we first clear + // the last error/result from the `observableQuery` before re-starting + // the subscription, and restore it afterwards (so the subscription + // has a chance to stay open). + try { + obsQuery.resetLastResults(); + subscription = obsQuery.subscribe(onNext, onError); + } finally { + obsQuery["last"] = last; + } - // TODO Remove this method when we remove support for options.partialRefetch. - this.unsafeHandlePartialRefetch(result); + if (!hasOwnProperty.call(error, 'graphQLErrors')) { + // The error is not a GraphQL error + throw error; + } - return this.toQueryResult(result); + const previousResult = this.result; + if ( + !previousResult || + (previousResult && previousResult.loading) || + !equal(error, previousResult.error) + ) { + this.setResult({ + data: (previousResult && previousResult.data) as TData, + error: error as ApolloError, + loading: false, + networkStatus: NetworkStatus.error, + }); + } + }; + + let subscription = obsQuery.subscribe(onNext, onError); + this.forceUpdate = onStoreChange; + return () => { + subscription.unsubscribe(); + }; + }, + () => { + const result = this.getCurrentResult(); + // TODO Remove this method when we remove support for options.partialRefetch. + this.unsafeHandlePartialRefetch(result); + return this.toQueryResult(result); + }, + ); } // These members (except for renderPromises) are all populated by the @@ -354,84 +421,6 @@ class InternalState { return obsQuery; } - private useSubscriptionEffect( - // We could use this.observable and not pass this obsQuery parameter, but I - // like the guarantee that obsQuery won't change, whereas this.observable - // could change without warning (in theory). - obsQuery: ObservableQuery, - ) { - // An effect to subscribe to the current observable query - useEffect(() => { - if (this.renderPromises) { - return; - } - - const onNext = () => { - const previousResult = this.result; - // We use `getCurrentResult()` instead of the onNext argument because - // the values differ slightly. Specifically, loading results will have - // an empty object for data instead of `undefined` for some reason. - const result = obsQuery.getCurrentResult(); - // Make sure we're not attempting to re-render similar results - if ( - previousResult && - previousResult.loading === result.loading && - previousResult.networkStatus === result.networkStatus && - equal(previousResult.data, result.data) - ) { - return; - } - - this.setResult(result); - }; - - const onError = (error: Error) => { - const last = obsQuery["last"]; - subscription.unsubscribe(); - // Unfortunately, if `lastError` is set in the current - // `observableQuery` when the subscription is re-created, - // the subscription will immediately receive the error, which will - // cause it to terminate again. To avoid this, we first clear - // the last error/result from the `observableQuery` before re-starting - // the subscription, and restore it afterwards (so the subscription - // has a chance to stay open). - try { - obsQuery.resetLastResults(); - subscription = obsQuery.subscribe(onNext, onError); - } finally { - obsQuery["last"] = last; - } - - if (!hasOwnProperty.call(error, 'graphQLErrors')) { - // The error is not a GraphQL error - throw error; - } - - const previousResult = this.result; - if ( - !previousResult || - (previousResult && previousResult.loading) || - !equal(error, previousResult.error) - ) { - this.setResult({ - data: (previousResult && previousResult.data) as TData, - error: error as ApolloError, - loading: false, - networkStatus: NetworkStatus.error, - }); - } - }; - - let subscription = obsQuery.subscribe(onNext, onError); - - return () => subscription.unsubscribe(); - }, [ - obsQuery, - this.renderPromises, - this.client.disableNetworkFetches, - ]); - } - // These members are populated by getCurrentResult and setResult, and it's // okay/normal for them to be initially undefined. private result: undefined | ApolloQueryResult;