From baeb69cc082c78a936e8ac4b0119afc19ca8567e Mon Sep 17 00:00:00 2001 From: Brian Kim Date: Thu, 12 Aug 2021 12:53:35 -0400 Subject: [PATCH] Delete OperationData, QueryData, MutationData, SubscriptionData, useBaseQuery, useDeepMemo, useFastRefresh The Lannisters send their regards --- config/entryPoints.js | 1 - src/__tests__/__snapshots__/exports.ts.snap | 9 - src/__tests__/exports.ts | 2 - src/react/data/MutationData.ts | 172 ------ src/react/data/OperationData.ts | 80 --- src/react/data/QueryData.ts | 536 ------------------- src/react/data/SubscriptionData.ts | 156 ------ src/react/data/index.ts | 4 - src/react/hooks/utils/useAfterFastRefresh.ts | 29 - src/react/hooks/utils/useBaseQuery.ts | 97 ---- src/react/hooks/utils/useDeepMemo.ts | 22 - src/react/ssr/RenderPromises.ts | 12 +- 12 files changed, 9 insertions(+), 1111 deletions(-) delete mode 100644 src/react/data/MutationData.ts delete mode 100644 src/react/data/OperationData.ts delete mode 100644 src/react/data/QueryData.ts delete mode 100644 src/react/data/SubscriptionData.ts delete mode 100644 src/react/data/index.ts delete mode 100644 src/react/hooks/utils/useAfterFastRefresh.ts delete mode 100644 src/react/hooks/utils/useBaseQuery.ts delete mode 100644 src/react/hooks/utils/useDeepMemo.ts diff --git a/config/entryPoints.js b/config/entryPoints.js index 71c05482311..99d3a46485d 100644 --- a/config/entryPoints.js +++ b/config/entryPoints.js @@ -17,7 +17,6 @@ const entryPoints = [ { dirs: ['react'] }, { dirs: ['react', 'components'] }, { dirs: ['react', 'context'] }, - { dirs: ['react', 'data'] }, { dirs: ['react', 'hoc'] }, { dirs: ['react', 'hooks'] }, { dirs: ['react', 'parser'] }, diff --git a/src/__tests__/__snapshots__/exports.ts.snap b/src/__tests__/__snapshots__/exports.ts.snap index e8198b071cf..f797e80a0ce 100644 --- a/src/__tests__/__snapshots__/exports.ts.snap +++ b/src/__tests__/__snapshots__/exports.ts.snap @@ -256,15 +256,6 @@ Array [ ] `; -exports[`exports of public entry points @apollo/client/react/data 1`] = ` -Array [ - "MutationData", - "OperationData", - "QueryData", - "SubscriptionData", -] -`; - exports[`exports of public entry points @apollo/client/react/hoc 1`] = ` Array [ "graphql", diff --git a/src/__tests__/exports.ts b/src/__tests__/exports.ts index 13e00747a14..88ca3b43640 100644 --- a/src/__tests__/exports.ts +++ b/src/__tests__/exports.ts @@ -16,7 +16,6 @@ import * as linkWS from "../link/ws"; import * as react from "../react"; import * as reactComponents from "../react/components"; import * as reactContext from "../react/context"; -import * as reactData from "../react/data"; import * as reactHOC from "../react/hoc"; import * as reactHooks from "../react/hooks"; import * as reactParser from "../react/parser"; @@ -56,7 +55,6 @@ describe('exports of public entry points', () => { check("@apollo/client/react", react); check("@apollo/client/react/components", reactComponents); check("@apollo/client/react/context", reactContext); - check("@apollo/client/react/data", reactData); check("@apollo/client/react/hoc", reactHOC); check("@apollo/client/react/hooks", reactHooks); check("@apollo/client/react/parser", reactParser); diff --git a/src/react/data/MutationData.ts b/src/react/data/MutationData.ts deleted file mode 100644 index d16b45a2a96..00000000000 --- a/src/react/data/MutationData.ts +++ /dev/null @@ -1,172 +0,0 @@ -import { equal } from '@wry/equality'; - -import { DocumentType } from '../parser'; -import { ApolloError } from '../../errors'; -import { - MutationDataOptions, - MutationTuple, - MutationFunctionOptions, - MutationResult, -} from '../types/types'; -import { OperationData } from './OperationData'; -import { MutationOptions, mergeOptions, ApolloCache, OperationVariables, DefaultContext } from '../../core'; -import { FetchResult } from '../../link/core'; - -type MutationResultWithoutClient = Omit, 'client'>; - -export class MutationData< - TData = any, - TVariables = OperationVariables, - TContext = DefaultContext, - TCache extends ApolloCache = ApolloCache, -> extends OperationData> { - private mostRecentMutationId: number; - private result: MutationResultWithoutClient; - private previousResult?: MutationResultWithoutClient; - private setResult: (result: MutationResultWithoutClient) => any; - - constructor({ - options, - context, - result, - setResult - }: { - options: MutationDataOptions; - context: any; - result: MutationResultWithoutClient; - setResult: (result: MutationResultWithoutClient) => any; - }) { - super(options, context); - this.verifyDocumentType(options.mutation, DocumentType.Mutation); - this.result = result; - this.setResult = setResult; - this.mostRecentMutationId = 0; - } - - public execute(result: MutationResultWithoutClient): MutationTuple { - this.isMounted = true; - this.verifyDocumentType(this.getOptions().mutation, DocumentType.Mutation); - return [ - this.runMutation, - { ...result, client: this.refreshClient().client } - ] as MutationTuple; - } - - public afterExecute() { - this.isMounted = true; - return this.unmount.bind(this); - } - - public cleanup() { - // No cleanup required. - } - - private runMutation = ( - mutationFunctionOptions: MutationFunctionOptions< - TData, - TVariables, - TContext, - TCache - > = {} as MutationFunctionOptions - ) => { - this.onMutationStart(); - const mutationId = this.generateNewMutationId(); - - return this.mutate(mutationFunctionOptions) - .then((response: FetchResult) => { - this.onMutationCompleted(response, mutationId); - return response; - }) - .catch((error: ApolloError) => { - const { onError } = this.getOptions(); - this.onMutationError(error, mutationId); - if (onError) { - onError(error); - return { - data: undefined, - errors: error, - }; - } else { - throw error; - } - }); - }; - - private mutate( - options: MutationFunctionOptions - ) { - return this.refreshClient().client.mutate( - mergeOptions( - this.getOptions(), - options as MutationOptions, - ), - ); - } - - private onMutationStart() { - if (!this.result.loading && !this.getOptions().ignoreResults) { - this.updateResult({ - loading: true, - error: undefined, - data: undefined, - called: true - }); - } - } - - private onMutationCompleted( - response: FetchResult, - mutationId: number - ) { - const { onCompleted, ignoreResults } = this.getOptions(); - - const { data, errors } = response; - const error = - errors && errors.length > 0 - ? new ApolloError({ graphQLErrors: errors }) - : undefined; - - const callOncomplete = () => - onCompleted ? onCompleted(data as TData) : null; - - if (this.isMostRecentMutation(mutationId) && !ignoreResults) { - this.updateResult({ - called: true, - loading: false, - data, - error - }); - } - callOncomplete(); - } - - private onMutationError(error: ApolloError, mutationId: number) { - if (this.isMostRecentMutation(mutationId)) { - this.updateResult({ - loading: false, - error, - data: undefined, - called: true - }); - } - } - - private generateNewMutationId(): number { - return ++this.mostRecentMutationId; - } - - private isMostRecentMutation(mutationId: number) { - return this.mostRecentMutationId === mutationId; - } - - private updateResult(result: MutationResultWithoutClient): MutationResultWithoutClient | undefined { - if ( - this.isMounted && - (!this.previousResult || !equal(this.previousResult, result)) - ) { - this.setResult(result); - this.previousResult = result; - return result; - } - } -} diff --git a/src/react/data/OperationData.ts b/src/react/data/OperationData.ts deleted file mode 100644 index f6f6584ba8c..00000000000 --- a/src/react/data/OperationData.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { DocumentNode } from 'graphql'; -import { equal } from '@wry/equality'; -import { invariant } from 'ts-invariant'; - -import { ApolloClient } from '../../core'; -import { DocumentType, parser, operationName } from '../parser'; -import { CommonOptions } from '../types/types'; - -export abstract class OperationData { - public isMounted: boolean = false; - public previousOptions: CommonOptions = {} as CommonOptions< - TOptions - >; - public context: any = {}; - public client: ApolloClient; - - private options: CommonOptions = {} as CommonOptions; - - constructor(options?: CommonOptions, context?: any) { - this.options = options || ({} as CommonOptions); - this.context = context || {}; - } - - public getOptions(): CommonOptions { - return this.options; - } - - public setOptions( - newOptions: CommonOptions, - storePrevious: boolean = false - ) { - if (storePrevious && !equal(this.options, newOptions)) { - this.previousOptions = this.options; - } - this.options = newOptions; - } - - public abstract execute(...args: any): any; - public abstract afterExecute(...args: any): void | (() => void); - public abstract cleanup(): void; - - protected unmount() { - this.isMounted = false; - } - - protected refreshClient() { - const client = - (this.options && this.options.client) || - (this.context && this.context.client); - - invariant( - !!client, - 'Could not find "client" in the context or passed in as an option. ' + - 'Wrap the root component in an , or pass an ' + - 'ApolloClient instance in via options.' - ); - - let isNew = false; - if (client !== this.client) { - isNew = true; - this.client = client; - this.cleanup(); - } - return { - client: this.client as ApolloClient, - isNew - }; - } - - protected verifyDocumentType(document: DocumentNode, type: DocumentType) { - const operation = parser(document); - const requiredOperationName = operationName(type); - const usedOperationName = operationName(operation.type); - invariant( - operation.type === type, - `Running a ${requiredOperationName} requires a graphql ` + - `${requiredOperationName}, but a ${usedOperationName} was used instead.` - ); - } -} diff --git a/src/react/data/QueryData.ts b/src/react/data/QueryData.ts deleted file mode 100644 index 7975af5fbd4..00000000000 --- a/src/react/data/QueryData.ts +++ /dev/null @@ -1,536 +0,0 @@ -import { equal } from '@wry/equality'; - -import { ApolloError } from '../../errors'; - -import { - ApolloClient, - NetworkStatus, - FetchMoreQueryOptions, - SubscribeToMoreOptions, - ObservableQuery, - FetchMoreOptions, - UpdateQueryOptions, - DocumentNode, - TypedDocumentNode, -} from '../../core'; - -import { - ObservableSubscription -} from '../../utilities'; - -import { DocumentType } from '../parser'; -import { - QueryResult, - QueryDataOptions, - QueryTuple, - QueryLazyOptions, - ObservableQueryFields, -} from '../types/types'; -import { OperationData } from './OperationData'; - -type ObservableQueryOptions = - ReturnType["prepareObservableQueryOptions"]>; - -export class QueryData extends OperationData< - QueryDataOptions -> { - public onNewData: () => void; - public currentObservable?: ObservableQuery; - private currentSubscription?: ObservableSubscription; - private runLazy: boolean = false; - private lazyOptions?: QueryLazyOptions; - private previous: { - client?: ApolloClient; - query?: DocumentNode | TypedDocumentNode; - observableQueryOptions?: ObservableQueryOptions; - result?: QueryResult; - loading?: boolean; - options?: QueryDataOptions; - error?: ApolloError; - } = Object.create(null); - - constructor({ - options, - context, - onNewData - }: { - options: QueryDataOptions; - context: any; - onNewData: () => void; - }) { - super(options, context); - this.onNewData = onNewData; - } - - public execute(): QueryResult { - this.refreshClient(); - - const { skip, query } = this.getOptions(); - if (skip || query !== this.previous.query) { - this.removeQuerySubscription(); - this.removeObservable(!skip); - this.previous.query = query; - } - - this.updateObservableQuery(); - - return this.getExecuteSsrResult() || this.getExecuteResult(); - } - - public executeLazy(): QueryTuple { - return !this.runLazy - ? [ - this.runLazyQuery, - { - loading: false, - networkStatus: NetworkStatus.ready, - called: false, - data: undefined - } - ] - : [this.runLazyQuery, this.execute()]; - } - - // For server-side rendering - public fetchData(): Promise | boolean { - const options = this.getOptions(); - if (options.skip || options.ssr === false) return false; - return new Promise(resolve => this.startQuerySubscription(resolve)); - } - - public afterExecute({ lazy = false }: { lazy?: boolean } = {}) { - this.isMounted = true; - const options = this.getOptions(); - if ( - this.currentObservable && - !this.ssrInitiated() && - !this.client.disableNetworkFetches - ) { - this.startQuerySubscription(); - } - - if (!lazy || this.runLazy) { - this.handleErrorOrCompleted(); - } - - this.previousOptions = options; - return this.unmount.bind(this); - } - - public cleanup() { - this.removeQuerySubscription(); - this.removeObservable(true); - delete this.previous.result; - } - - public getOptions() { - const options = super.getOptions(); - - if (this.lazyOptions) { - options.variables = { - ...options.variables, - ...this.lazyOptions.variables - } as TVariables; - options.context = { - ...options.context, - ...this.lazyOptions.context - }; - } - - // skip is not supported when using lazy query execution. - if (this.runLazy) { - delete options.skip; - } - - return options; - } - - public ssrInitiated() { - return this.context && this.context.renderPromises; - } - - private runLazyQuery = (options?: QueryLazyOptions) => { - this.cleanup(); - this.runLazy = true; - this.lazyOptions = options; - this.onNewData(); - }; - - private getExecuteSsrResult() { - const { ssr, skip } = this.getOptions(); - const ssrDisabled = ssr === false; - const fetchDisabled = this.refreshClient().client.disableNetworkFetches; - - const ssrLoading = { - loading: true, - networkStatus: NetworkStatus.loading, - called: true, - data: undefined, - stale: false, - client: this.client, - ...this.observableQueryFields(), - } as QueryResult; - - // If SSR has been explicitly disabled, and this function has been called - // on the server side, return the default loading state. - if (ssrDisabled && (this.ssrInitiated() || fetchDisabled)) { - this.previous.result = ssrLoading; - return ssrLoading; - } - - if (this.ssrInitiated()) { - const result = this.getExecuteResult() || ssrLoading; - if (result.loading && !skip) { - this.context.renderPromises!.addQueryPromise(this, () => null); - } - return result; - } - } - - private prepareObservableQueryOptions() { - const options = this.getOptions(); - this.verifyDocumentType(options.query, DocumentType.Query); - const displayName = options.displayName || 'Query'; - - // Set the fetchPolicy to cache-first for network-only and cache-and-network - // fetches for server side renders. - if ( - this.ssrInitiated() && - (options.fetchPolicy === 'network-only' || - options.fetchPolicy === 'cache-and-network') - ) { - options.fetchPolicy = 'cache-first'; - } - - return { - ...options, - displayName, - context: options.context, - }; - } - - private initializeObservableQuery() { - // See if there is an existing observable that was used to fetch the same - // data and if so, use it instead since it will contain the proper queryId - // to fetch the result set. This is used during SSR. - if (this.ssrInitiated()) { - this.currentObservable = this.context!.renderPromises!.getSSRObservable( - this.getOptions() - ); - } - - if (!this.currentObservable) { - const observableQueryOptions = this.prepareObservableQueryOptions(); - - this.previous.observableQueryOptions = { - ...observableQueryOptions, - children: void 0, - }; - this.currentObservable = this.refreshClient().client.watchQuery({ - ...observableQueryOptions - }); - - if (this.ssrInitiated()) { - this.context!.renderPromises!.registerSSRObservable( - this.currentObservable, - observableQueryOptions - ); - } - } - } - - private updateObservableQuery() { - // If we skipped initially, we may not have yet created the observable - if (!this.currentObservable) { - this.initializeObservableQuery(); - return; - } - - const newObservableQueryOptions = { - ...this.prepareObservableQueryOptions(), - children: void 0, - }; - - if (this.getOptions().skip) { - this.previous.observableQueryOptions = newObservableQueryOptions; - return; - } - - if ( - !equal(newObservableQueryOptions, this.previous.observableQueryOptions) - ) { - this.previous.observableQueryOptions = newObservableQueryOptions; - this.currentObservable - .setOptions(newObservableQueryOptions) - // The error will be passed to the child container, so we don't - // need to log it here. We could conceivably log something if - // an option was set. OTOH we don't log errors w/ the original - // query. See https://github.com/apollostack/react-apollo/issues/404 - .catch(() => {}); - } - } - - // Setup a subscription to watch for Apollo Client `ObservableQuery` changes. - // When new data is received, and it doesn't match the data that was used - // during the last `QueryData.execute` call (and ultimately the last query - // component render), trigger the `onNewData` callback. If not specified, - // `onNewData` will fallback to the default `QueryData.onNewData` function - // (which usually leads to a query component re-render). - private startQuerySubscription(onNewData: () => void = this.onNewData) { - if (this.currentSubscription || this.getOptions().skip) return; - - this.currentSubscription = this.currentObservable!.subscribe({ - next: ({ loading, networkStatus, data }) => { - const previousResult = this.previous.result; - - // Make sure we're not attempting to re-render similar results - if ( - previousResult && - previousResult.loading === loading && - previousResult.networkStatus === networkStatus && - equal(previousResult.data, data) - ) { - return; - } - - onNewData(); - }, - error: error => { - this.resubscribeToQuery(); - if (!error.hasOwnProperty('graphQLErrors')) throw error; - - const previousResult = this.previous.result; - if ( - (previousResult && previousResult.loading) || - !equal(error, this.previous.error) - ) { - this.previous.error = error; - onNewData(); - } - } - }); - } - - private resubscribeToQuery() { - this.removeQuerySubscription(); - - // 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). - const { currentObservable } = this; - if (currentObservable) { - const lastError = currentObservable.getLastError(); - const lastResult = currentObservable.getLastResult(); - currentObservable.resetLastResults(); - this.startQuerySubscription(); - Object.assign(currentObservable, { - lastError, - lastResult - }); - } - } - - private getExecuteResult(): QueryResult { - let result = this.observableQueryFields() as QueryResult; - const options = this.getOptions(); - - // When skipping a query (ie. we're not querying for data but still want - // to render children), make sure the `data` is cleared out and - // `loading` is set to `false` (since we aren't loading anything). - // - // NOTE: We no longer think this is the correct behavior. Skipping should - // not automatically set `data` to `undefined`, but instead leave the - // previous data in place. In other words, skipping should not mandate - // that previously received data is all of a sudden removed. Unfortunately, - // changing this is breaking, so we'll have to wait until Apollo Client - // 4.0 to address this. - if (options.skip) { - result = { - ...result, - data: undefined, - error: undefined, - loading: false, - networkStatus: NetworkStatus.ready, - called: true, - }; - } else if (this.currentObservable) { - // Fetch the current result (if any) from the store. - const currentResult = this.currentObservable.getCurrentResult(); - const { data, loading, partial, networkStatus, errors } = currentResult; - let { error } = currentResult; - - // Until a set naming convention for networkError and graphQLErrors is - // decided upon, we map errors (graphQLErrors) to the error options. - if (errors && errors.length > 0) { - error = new ApolloError({ graphQLErrors: errors }); - } - - result = { - ...result, - data, - loading, - networkStatus, - error, - called: true - }; - - if (loading) { - // Fall through without modifying result... - } else if (error) { - Object.assign(result, { - data: (this.currentObservable.getLastResult() || ({} as any)) - .data - }); - } else { - const { fetchPolicy } = this.currentObservable.options; - const { partialRefetch } = options; - if ( - partialRefetch && - partial && - (!data || Object.keys(data).length === 0) && - fetchPolicy !== 'cache-only' - ) { - // When a `Query` component is mounted, and a mutation is executed - // that returns the same ID as the mounted `Query`, but has less - // fields in its result, Apollo Client's `QueryManager` returns the - // data as `undefined` since a hit can't be found in the cache. - // This can lead to application errors when the UI elements rendered by - // the original `Query` component are expecting certain data values to - // exist, and they're all of a sudden stripped away. To help avoid - // this we'll attempt to refetch the `Query` data. - Object.assign(result, { - loading: true, - networkStatus: NetworkStatus.loading - }); - result.refetch(); - return result; - } - } - } - - result.client = this.client; - // Store options as this.previousOptions. - this.setOptions(options, true); - - const previousResult = this.previous.result; - - this.previous.loading = - previousResult && previousResult.loading || false; - - // Ensure the returned result contains previousData as a separate - // property, to give developers the flexibility of leveraging outdated - // data while new data is loading from the network. Falling back to - // previousResult.previousData when previousResult.data is falsy here - // allows result.previousData to persist across multiple results. - result.previousData = previousResult && - (previousResult.data || previousResult.previousData); - - this.previous.result = result; - - // Any query errors that exist are now available in `result`, so we'll - // remove the original errors from the `ObservableQuery` query store to - // make sure they aren't re-displayed on subsequent (potentially error - // free) requests/responses. - this.currentObservable && this.currentObservable.resetQueryStoreErrors(); - - return result; - } - - private handleErrorOrCompleted() { - if (!this.currentObservable || !this.previous.result) return; - - const { data, loading, error } = this.previous.result; - - if (!loading) { - const { - query, - variables, - onCompleted, - onError, - skip - } = this.getOptions(); - - // No changes, so we won't call onError/onCompleted. - if ( - this.previousOptions && - !this.previous.loading && - equal(this.previousOptions.query, query) && - equal(this.previousOptions.variables, variables) - ) { - return; - } - - if (onCompleted && !error && !skip) { - onCompleted(data as TData); - } else if (onError && error) { - onError(error); - } - } - } - - private removeQuerySubscription() { - if (this.currentSubscription) { - this.currentSubscription.unsubscribe(); - delete this.currentSubscription; - } - } - - private removeObservable(andDelete: boolean) { - if (this.currentObservable) { - this.currentObservable["tearDownQuery"](); - if (andDelete) { - delete this.currentObservable; - } - } - } - - private obsRefetch = (variables?: Partial) => - this.currentObservable?.refetch(variables); - - private obsFetchMore = ( - fetchMoreOptions: FetchMoreQueryOptions & - FetchMoreOptions - ) => this.currentObservable?.fetchMore(fetchMoreOptions); - - private obsUpdateQuery = ( - mapFn: ( - previousQueryResult: TData, - options: UpdateQueryOptions - ) => TData - ) => this.currentObservable?.updateQuery(mapFn); - - private obsStartPolling = (pollInterval: number) => { - this.currentObservable?.startPolling(pollInterval); - }; - - private obsStopPolling = () => { - this.currentObservable?.stopPolling(); - }; - - private obsSubscribeToMore = < - TSubscriptionData = TData, - TSubscriptionVariables = TVariables - >( - options: SubscribeToMoreOptions< - TData, - TSubscriptionVariables, - TSubscriptionData - > - ) => this.currentObservable?.subscribeToMore(options); - - private observableQueryFields() { - return { - variables: this.currentObservable?.variables, - refetch: this.obsRefetch, - fetchMore: this.obsFetchMore, - updateQuery: this.obsUpdateQuery, - startPolling: this.obsStartPolling, - stopPolling: this.obsStopPolling, - subscribeToMore: this.obsSubscribeToMore - } as ObservableQueryFields; - } -} diff --git a/src/react/data/SubscriptionData.ts b/src/react/data/SubscriptionData.ts deleted file mode 100644 index 87fd89b97b3..00000000000 --- a/src/react/data/SubscriptionData.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { equal } from '@wry/equality'; - -import { OperationData } from './OperationData'; -import { - SubscriptionCurrentObservable, - SubscriptionDataOptions, - SubscriptionResult -} from '../types/types'; - -export class SubscriptionData< - TData = any, - TVariables = any -> extends OperationData> { - private setResult: any; - private currentObservable: SubscriptionCurrentObservable = {}; - - constructor({ - options, - context, - setResult - }: { - options: SubscriptionDataOptions; - context: any; - setResult: any; - }) { - super(options, context); - this.setResult = setResult; - this.initialize(options); - } - - public execute(result: SubscriptionResult) { - if (this.getOptions().skip === true) { - this.cleanup(); - return { - loading: false, - error: undefined, - data: undefined, - variables: this.getOptions().variables - }; - } - - let currentResult = result; - if (this.refreshClient().isNew) { - currentResult = this.getLoadingResult(); - } - - let { shouldResubscribe } = this.getOptions(); - if (typeof shouldResubscribe === 'function') { - shouldResubscribe = !!shouldResubscribe(this.getOptions()); - } - - if ( - shouldResubscribe !== false && - this.previousOptions && - Object.keys(this.previousOptions).length > 0 && - (this.previousOptions.subscription !== this.getOptions().subscription || - !equal(this.previousOptions.variables, this.getOptions().variables) || - this.previousOptions.skip !== this.getOptions().skip) - ) { - this.cleanup(); - currentResult = this.getLoadingResult(); - } - - this.initialize(this.getOptions()); - this.startSubscription(); - - this.previousOptions = this.getOptions(); - return { ...currentResult, variables: this.getOptions().variables }; - } - - public afterExecute() { - this.isMounted = true; - } - - public cleanup() { - this.endSubscription(); - delete this.currentObservable.query; - } - - private initialize(options: SubscriptionDataOptions) { - if (this.currentObservable.query || this.getOptions().skip === true) return; - this.currentObservable.query = this.refreshClient().client.subscribe({ - query: options.subscription, - variables: options.variables, - fetchPolicy: options.fetchPolicy, - context: options.context, - }); - } - - private startSubscription() { - if (this.currentObservable.subscription) return; - this.currentObservable.subscription = this.currentObservable.query!.subscribe( - { - next: this.updateCurrentData.bind(this), - error: this.updateError.bind(this), - complete: this.completeSubscription.bind(this) - } - ); - } - - private getLoadingResult() { - return { - loading: true, - error: undefined, - data: undefined - } as SubscriptionResult; - } - - private updateResult(result: SubscriptionResult) { - if (this.isMounted) { - this.setResult(result); - } - } - - private updateCurrentData(result: SubscriptionResult) { - const { onSubscriptionData } = this.getOptions(); - - this.updateResult({ - data: result.data, - loading: false, - error: undefined - }); - - if (onSubscriptionData) { - onSubscriptionData({ - client: this.refreshClient().client, - subscriptionData: result - }); - } - } - - private updateError(error: any) { - this.updateResult({ - error, - loading: false - }); - } - - private completeSubscription() { - // We have to defer this endSubscription call, because otherwise multiple - // subscriptions for the same component will cause infinite rendering. - // See https://github.com/apollographql/apollo-client/pull/7917 - Promise.resolve().then(() => { - const { onSubscriptionComplete } = this.getOptions(); - if (onSubscriptionComplete) onSubscriptionComplete(); - this.endSubscription(); - }); - } - - private endSubscription() { - if (this.currentObservable.subscription) { - this.currentObservable.subscription.unsubscribe(); - delete this.currentObservable.subscription; - } - } -} diff --git a/src/react/data/index.ts b/src/react/data/index.ts deleted file mode 100644 index 26776e66350..00000000000 --- a/src/react/data/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { SubscriptionData } from './SubscriptionData'; -export { OperationData } from './OperationData'; -export { MutationData } from './MutationData'; -export { QueryData } from './QueryData'; diff --git a/src/react/hooks/utils/useAfterFastRefresh.ts b/src/react/hooks/utils/useAfterFastRefresh.ts deleted file mode 100644 index de8742f398e..00000000000 --- a/src/react/hooks/utils/useAfterFastRefresh.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { useEffect, useRef } from "react"; - -/** - * This hook allows running a function only immediately after a react - * fast refresh or live reload. - * - * Useful in order to ensure that we can reinitialize things that have been - * disposed by cleanup functions in `useEffect`. - * @param effectFn a function to run immediately after a fast refresh - */ -export function useAfterFastRefresh(effectFn: () => unknown) { - if (__DEV__) { - const didRefresh = useRef(false); - useEffect(() => { - return () => { - // Detect fast refresh, only runs multiple times in fast refresh - didRefresh.current = true; - }; - }, []); - - useEffect(() => { - if (didRefresh.current === true) { - // This block only runs after a fast refresh - didRefresh.current = false; - effectFn(); - } - }, []) - } -} diff --git a/src/react/hooks/utils/useBaseQuery.ts b/src/react/hooks/utils/useBaseQuery.ts deleted file mode 100644 index 48f5419dd4f..00000000000 --- a/src/react/hooks/utils/useBaseQuery.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { useContext, useEffect, useReducer, useRef } from 'react'; -import { DocumentNode } from 'graphql'; -import { TypedDocumentNode } from '@graphql-typed-document-node/core'; - -import { - QueryHookOptions, - QueryDataOptions, - QueryTuple, - QueryResult, -} from '../../types/types'; -import { QueryData } from '../../data'; -import { useDeepMemo } from './useDeepMemo'; -import { OperationVariables } from '../../../core'; -import { getApolloContext } from '../../context'; -import { useAfterFastRefresh } from './useAfterFastRefresh'; - -export function useBaseQuery( - query: DocumentNode | TypedDocumentNode, - options?: QueryHookOptions, - lazy = false -) { - const context = useContext(getApolloContext()); - const [tick, forceUpdate] = useReducer(x => x + 1, 0); - const updatedOptions = options ? { ...options, query } : { query }; - - const queryDataRef = useRef>(); - const queryData = queryDataRef.current || ( - queryDataRef.current = new QueryData({ - options: updatedOptions as QueryDataOptions, - context, - onNewData() { - if (!queryData.ssrInitiated()) { - // When new data is received from the `QueryData` object, we want to - // force a re-render to make sure the new data is displayed. We can't - // force that re-render if we're already rendering however so to be - // safe we'll trigger the re-render in a microtask. In case the - // component gets unmounted before this callback fires, we re-check - // queryDataRef.current.isMounted before calling forceUpdate(). - Promise.resolve().then(() => queryDataRef.current && queryDataRef.current.isMounted && forceUpdate()); - } else { - // If we're rendering on the server side we can force an update at - // any point. - forceUpdate(); - } - } - }) - ); - - queryData.setOptions(updatedOptions); - queryData.context = context; - - // `onError` and `onCompleted` callback functions will not always have a - // stable identity, so we'll exclude them from the memoization key to - // prevent `afterExecute` from being triggered un-necessarily. - const memo = { - options: { - ...updatedOptions, - onError: void 0, - onCompleted: void 0 - } as QueryHookOptions, - context, - tick - }; - - const result = useDeepMemo( - () => (lazy ? queryData.executeLazy() : queryData.execute()), - memo - ); - - const queryResult = lazy - ? (result as QueryTuple)[1] - : (result as QueryResult); - - if (__DEV__) { - // ensure we run an update after refreshing so that we reinitialize - useAfterFastRefresh(forceUpdate); - } - - useEffect(() => { - return () => { - queryData.cleanup(); - // this effect can run multiple times during a fast-refresh - // so make sure we clean up the ref - queryDataRef.current = void 0; - } - }, []); - - useEffect(() => queryData.afterExecute({ lazy }), [ - queryResult.loading, - queryResult.networkStatus, - queryResult.error, - queryResult.data, - queryData.currentObservable, - ]); - - return result; -} diff --git a/src/react/hooks/utils/useDeepMemo.ts b/src/react/hooks/utils/useDeepMemo.ts deleted file mode 100644 index 868804e1bd4..00000000000 --- a/src/react/hooks/utils/useDeepMemo.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { useRef } from 'react'; -import { equal } from '@wry/equality'; - -/** - * Memoize a result using deep equality. This hook has two advantages over - * React.useMemo: it uses deep equality to compare memo keys, and it guarantees - * that the memo function will only be called if the keys are unequal. - * React.useMemo cannot be relied on to do this, since it is only a performance - * optimization (see https://reactjs.org/docs/hooks-reference.html#usememo). - */ -export function useDeepMemo( - memoFn: () => TValue, - key: TKey -): TValue { - const ref = useRef<{ key: TKey; value: TValue }>(); - - if (!ref.current || !equal(key, ref.current.key)) { - ref.current = { key, value: memoFn() }; - } - - return ref.current.value; -} diff --git a/src/react/ssr/RenderPromises.ts b/src/react/ssr/RenderPromises.ts index 6bc06826051..fb74c87c304 100644 --- a/src/react/ssr/RenderPromises.ts +++ b/src/react/ssr/RenderPromises.ts @@ -2,7 +2,13 @@ import { DocumentNode } from 'graphql'; import { ObservableQuery } from '../../core'; import { QueryDataOptions } from '../types/types'; -import { QueryData } from '../data/QueryData'; + +// TODO: A vestigial interface from when hooks were implemented with utility +// classes, which should be deleted in the future. +interface QueryData { + getOptions(): any; + fetchData(): Promise; +} type QueryInfo = { seen: boolean; @@ -51,8 +57,8 @@ export class RenderPromises { return this.lookupQueryInfo(props).observable; } - public addQueryPromise( - queryInstance: QueryData, + public addQueryPromise( + queryInstance: QueryData, finish: () => React.ReactNode ): React.ReactNode { if (!this.stopped) {