Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor useLazyQuery to reuse useInternalState and make execution function call reobserve instead of refetch #9564

Merged
merged 23 commits into from
Apr 5, 2022
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
7fc6c89
Improve various (passing) tests.
benjamn Mar 15, 2022
3546c9c
Improve useLazyQuery-related types.
benjamn Mar 14, 2022
02a1e6d
Reuse useInternalState for useLazyQuery.
benjamn Mar 15, 2022
a18e2e0
Avoid needlessly overriding internalState.useQuery QueryResult.
benjamn Mar 15, 2022
2302716
Reuse internalState.toQueryResult for useLazyQuery execute function.
benjamn Mar 15, 2022
4e77bca
Use useRef instead of useState for useLazyQuery execution state.
benjamn Mar 15, 2022
72a805d
Use execOptionsRef.current to represent both options and called-ness.
benjamn Mar 15, 2022
f93e904
Use reobserve rather than refetch for useLazyQuery exec function.
benjamn Mar 15, 2022
d26bdd4
Make options parameter to applyFetchPolicy non-optional.
benjamn Mar 22, 2022
52ba67e
Allow NetworkStatus.setVariables inference when fetchPolicy unchanged.
benjamn Mar 22, 2022
6cb2cdb
Backup ApolloQueryResult for standby lazy executions.
benjamn Mar 17, 2022
ff0186d
Always mark NetworkStatus.ready after network fetch.
benjamn Mar 24, 2022
4be1901
Simplify useLazyQuery initial fetchPolicy/skip options.
benjamn Mar 17, 2022
71d8df4
Reject useLazyQuery execution Promise on error.
benjamn Mar 29, 2022
345a156
Call internalState.forceUpdate in eager methods only if first call.
benjamn Mar 29, 2022
f164b1b
Use manual wrapping rather than Function.prototype.bind.
benjamn Mar 29, 2022
89af070
Provide use[Lazy]Query(...).reobserve in addition to refetch.
benjamn Apr 1, 2022
782642b
Revert manual wrapping of ObservableQueryMethods in favor of bind.
benjamn Apr 4, 2022
c3d1a83
Remove unnecessary public modifiers from InternalState methods.
benjamn Apr 4, 2022
da41ce8
Allow specifying WatchQueryOptions.initialFetchPolicy explicitly.
benjamn Apr 4, 2022
c9a2c01
Bring back exported QueryTuple type as alias for LazyQueryResultTuple.
benjamn Apr 4, 2022
feb07db
Make TODO comment easier to grep for.
benjamn Apr 4, 2022
ff43d78
Mention PR #9564 in CHANGELOG.md.
benjamn Apr 4, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/__tests__/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3321,7 +3321,7 @@ describe('@connection', () => {
expect(context.reason).toBe("after-fetch");
expect(context.observable).toBe(obs);
expect(context.options).toBe(obs.options);
expect(context.initialPolicy).toBe("cache-first");
expect(context.initialFetchPolicy).toBe("cache-first");

// Usually options.nextFetchPolicy applies only once, but a
// nextFetchPolicy function can set this.nextFetchPolicy
Expand Down
38 changes: 21 additions & 17 deletions src/core/ObservableQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import {
WatchQueryOptions,
FetchMoreQueryOptions,
SubscribeToMoreOptions,
WatchQueryFetchPolicy,
NextFetchPolicyContext,
} from './watchQueryOptions';
import { QueryInfo } from './QueryInfo';
Expand Down Expand Up @@ -72,10 +71,6 @@ export class ObservableQuery<
return this.options.variables;
}

// Original value of this.options.fetchPolicy (defaulting to "cache-first"),
// from whenever the ObservableQuery was first created.
private initialFetchPolicy: WatchQueryFetchPolicy;

private isTornDown: boolean;
private queryManager: QueryManager<any>;
private observers = new Set<Observer<ApolloQueryResult<TData>>>();
Expand Down Expand Up @@ -145,15 +140,19 @@ export class ObservableQuery<
// active state
this.isTornDown = false;

// query information
this.options = options;
this.options = {
// Remember the initial options.fetchPolicy so we can revert back to this
// policy when variables change. This information can also be specified
// (or overridden) by providing options.initialFetchPolicy explicitly.
initialFetchPolicy: options.fetchPolicy || "cache-first",
...options,
};

this.queryId = queryInfo.queryId || queryManager.generateQueryId();

const opDef = getOperationDefinition(options.query);
this.queryName = opDef && opDef.name && opDef.name.value;

this.initialFetchPolicy = options.fetchPolicy || "cache-first";

// related classes
this.queryManager = queryManager;
this.queryInfo = queryInfo;
Expand Down Expand Up @@ -565,7 +564,7 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`);

return this.reobserve({
// Reset options.fetchPolicy to its original value.
fetchPolicy: this.initialFetchPolicy,
fetchPolicy: this.options.initialFetchPolicy,
variables,
}, NetworkStatus.setVariables);
}
Expand Down Expand Up @@ -616,10 +615,13 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`);
// options.fetchPolicy even if options !== this.options, though that happens
// most often when the options are temporary, used for only one request and
// then thrown away, so nextFetchPolicy may not end up mattering.
options: WatchQueryOptions<TVariables, TData> = this.options,
options: WatchQueryOptions<TVariables, TData>,
) {
if (options.nextFetchPolicy) {
const { fetchPolicy = "cache-first" } = options;
const {
fetchPolicy = "cache-first",
initialFetchPolicy = fetchPolicy,
} = options;

// When someone chooses "cache-and-network" or "network-only" as their
// initial FetchPolicy, they often do not want future cache updates to
Expand All @@ -631,15 +633,16 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`);
// options.nextFetchPolicy option provides an easy way to update
// options.fetchPolicy after the initial network request, without having to
// call observableQuery.setOptions.

if (typeof options.nextFetchPolicy === "function") {
options.fetchPolicy = options.nextFetchPolicy(fetchPolicy, {
reason,
options,
observable: this,
initialPolicy: this.initialFetchPolicy,
initialFetchPolicy,
});
} else if (reason === "variables-changed") {
options.fetchPolicy = this.initialFetchPolicy;
options.fetchPolicy = initialFetchPolicy;
} else {
options.fetchPolicy = options.nextFetchPolicy;
}
Expand Down Expand Up @@ -755,6 +758,7 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`);

// Save the old variables, since Object.assign may modify them below.
const oldVariables = this.options.variables;
const oldFetchPolicy = this.options.fetchPolicy;

const mergedOptions = mergeOptions(this.options, newOptions || {});
const options = useDisposableConcast
Expand All @@ -772,10 +776,10 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`);
if (
newOptions &&
newOptions.variables &&
!newOptions.fetchPolicy &&
!equal(newOptions.variables, oldVariables)
!equal(newOptions.variables, oldVariables) &&
(!newOptions.fetchPolicy || newOptions.fetchPolicy === oldFetchPolicy)
) {
this.applyNextFetchPolicy("variables-changed");
this.applyNextFetchPolicy("variables-changed", options);
if (newNetworkStatus === void 0) {
newNetworkStatus = NetworkStatus.setVariables;
}
Expand Down
3 changes: 2 additions & 1 deletion src/core/QueryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1060,11 +1060,12 @@ export class QueryManager<TStore> {
const aqr: ApolloQueryResult<TData> = {
data: result.data,
loading: false,
networkStatus: queryInfo.networkStatus || NetworkStatus.ready,
networkStatus: NetworkStatus.ready,
};

if (hasErrors && options.errorPolicy !== "ignore") {
aqr.errors = result.errors;
aqr.networkStatus = NetworkStatus.error;
}

return aqr;
Expand Down
2 changes: 1 addition & 1 deletion src/core/__tests__/ObservableQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1199,7 +1199,7 @@ describe('ObservableQuery', () => {
});

expect(observable.options.fetchPolicy).toBe('cache-and-network');
expect(observable["initialFetchPolicy"]).toBe('cache-and-network');
expect(observable.options.initialFetchPolicy).toBe('cache-and-network');

subscribeAndCount(reject, observable, (handleCount, result) => {
expect(result.error).toBeUndefined();
Expand Down
6 changes: 4 additions & 2 deletions src/core/__tests__/fetchPolicies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -938,7 +938,8 @@ describe("nextFetchPolicy", () => {
expect(currentFetchPolicy).toBe(context.options.fetchPolicy);
switch (context.reason) {
case "variables-changed":
return context.initialPolicy;
expect(context.initialFetchPolicy).toBe(context.options.initialFetchPolicy);
return context.initialFetchPolicy;
default:
case "after-fetch":
return "cache-first";
Expand Down Expand Up @@ -1070,7 +1071,8 @@ describe("nextFetchPolicy", () => {
expect(currentFetchPolicy).toBe(context.options.fetchPolicy);
switch (context.reason) {
case "variables-changed":
return context.initialPolicy;
expect(context.initialFetchPolicy).toBe(context.options.initialFetchPolicy);
return context.initialFetchPolicy;
default:
case "after-fetch":
return "cache-first";
Expand Down
11 changes: 10 additions & 1 deletion src/core/watchQueryOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ export interface WatchQueryOptions<TVariables = OperationVariables, TData = any>
* Specifies the {@link FetchPolicy} to be used for this query.
*/
fetchPolicy?: WatchQueryFetchPolicy;

/**
* Specifies the {@link FetchPolicy} to be used after this query has completed.
*/
Expand All @@ -129,6 +130,14 @@ export interface WatchQueryOptions<TVariables = OperationVariables, TData = any>
currentFetchPolicy: WatchQueryFetchPolicy,
context: NextFetchPolicyContext<TData, TVariables>,
) => WatchQueryFetchPolicy);

/**
* Defaults to the initial value of options.fetchPolicy, but can be explicitly
* configured to specify the WatchQueryFetchPolicy to revert back to whenever
* variables change (unless nextFetchPolicy intervenes).
*/
initialFetchPolicy?: WatchQueryFetchPolicy;
Comment on lines +134 to +139
Copy link
Member Author

@benjamn benjamn Apr 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't love adding another optional field to WatchQueryOptions, but I truly do not expect anyone to need to use this field directly, and it helps us maintain the promise that skip: true is (more or less) synonymous with fetchPolicy: "standby", because it gives us a way to pass fetchPolicy: "standby" when skip: true without ending up stuck with initialFetchPolicy === "standby".


/**
* Specifies whether a {@link NetworkStatus.refetch} operation should merge
* incoming field data with existing data, or overwrite the existing data.
Expand All @@ -144,7 +153,7 @@ export interface NextFetchPolicyContext<TData, TVariables> {
| "variables-changed";
observable: ObservableQuery<TData, TVariables>;
options: WatchQueryOptions<TVariables, TData>;
initialPolicy: WatchQueryFetchPolicy;
initialFetchPolicy: WatchQueryFetchPolicy;
}

export interface FetchMoreQueryOptions<TVariables, TData = any> {
Expand Down
1 change: 1 addition & 0 deletions src/react/components/__tests__/client/Query.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ describe('Query component', () => {
observable,
fetchMore,
refetch,
reobserve,
startPolling,
stopPolling,
subscribeToMore,
Expand Down
17 changes: 11 additions & 6 deletions src/react/hooks/__tests__/useLazyQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { ApolloClient, InMemoryCache } from '../../../core';
import { ApolloProvider } from '../../../react';
import { MockedProvider, mockSingleLink } from '../../../testing';
import { useLazyQuery } from '../useLazyQuery';
import { QueryResult } from '../../types/types';

describe('useLazyQuery Hook', () => {
it('should hold query execution until manually triggered', async () => {
Expand Down Expand Up @@ -532,18 +533,22 @@ describe('useLazyQuery Hook', () => {
expect(result.current[1].loading).toBe(false);
expect(result.current[1].data).toBe(undefined);
const execute = result.current[0];
let executeResult: any;
setTimeout(() => {
executeResult = execute();

const executeResult = new Promise<QueryResult<any, any>>(resolve => {
setTimeout(() => resolve(execute()));
});

await waitForNextUpdate();
expect(result.current[1].loading).toBe(true);

await waitForNextUpdate();
expect(result.current[1].loading).toBe(false);
expect(result.current[1].data).toEqual({ hello: 'world' });
await expect(executeResult).resolves.toEqual(result.current[1]);
const latestRenderResult = result.current[1];
expect(latestRenderResult.loading).toBe(false);
expect(latestRenderResult.data).toEqual({ hello: 'world' });

return executeResult.then(finalResult => {
expect(finalResult).toEqual(latestRenderResult);
});
});

it('should have matching results from execution function and hook', async () => {
Expand Down
108 changes: 108 additions & 0 deletions src/react/hooks/__tests__/useQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { ApolloLink } from '../../../link/core';
import { itAsync, MockLink, MockedProvider, mockSingleLink } from '../../../testing';
import { useQuery } from '../useQuery';
import { useMutation } from '../useMutation';
import { QueryResult } from '../../types/types';

describe('useQuery Hook', () => {
describe('General use', () => {
Expand Down Expand Up @@ -3374,6 +3375,113 @@ describe('useQuery Hook', () => {
networkStatus: 7,
});
});

it('should set correct initialFetchPolicy even if skip:true', async () => {
const query = gql`{ hello }`;
let linkCount = 0;
const link = new ApolloLink(() => Observable.of({
data: { hello: ++linkCount },
}));

const client = new ApolloClient({
cache: new InMemoryCache(),
link,
});

const correctInitialFetchPolicy: WatchQueryFetchPolicy =
"cache-and-network";

const { result, waitForNextUpdate, rerender } = renderHook<{
skip: boolean;
}, QueryResult>(
({ skip = true }) => useQuery(query, {
// Skipping equates to using a fetchPolicy of "standby", but that
// should not mean we revert to standby whenever we want to go back to
// the initial fetchPolicy (e.g. when variables change).
skip,
fetchPolicy: correctInitialFetchPolicy,
}),
{
initialProps: {
skip: true,
},
wrapper: ({ children }) => (
<ApolloProvider client={client}>
{children}
</ApolloProvider>
),
},
);

expect(result.current.loading).toBe(false);
expect(result.current.data).toBeUndefined();

function check(
expectedFetchPolicy: WatchQueryFetchPolicy,
expectedInitialFetchPolicy: WatchQueryFetchPolicy,
) {
const { observable } = result.current;
const {
fetchPolicy,
initialFetchPolicy,
} = observable.options;

expect(fetchPolicy).toBe(expectedFetchPolicy);
expect(initialFetchPolicy).toBe(expectedInitialFetchPolicy);
}

check(
"standby",
correctInitialFetchPolicy,
);

rerender({
skip: false,
});

await waitForNextUpdate();

expect(result.current.loading).toBe(false);
expect(result.current.data).toEqual({
hello: 1,
});

check(
correctInitialFetchPolicy,
correctInitialFetchPolicy,
);

const reasons: string[] = [];

const reobservePromise = result.current.observable.reobserve({
variables: {
newVar: true,
},
nextFetchPolicy(currentFetchPolicy, context) {
expect(currentFetchPolicy).toBe("cache-and-network");
expect(context.initialFetchPolicy).toBe("cache-and-network");
reasons.push(context.reason);
return currentFetchPolicy;
},
}).then(result => {
expect(result.loading).toBe(false);
expect(result.data).toEqual({ hello: 2 });
});

await waitForNextUpdate();

expect(result.current.loading).toBe(false);
expect(result.current.data).toEqual({
hello: 2,
});

await reobservePromise;

expect(reasons).toEqual([
"variables-changed",
"after-fetch",
]);
});
});

describe('Missing Fields', () => {
Expand Down
2 changes: 1 addition & 1 deletion src/react/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ import '../../utilities/globals';
export * from './useApolloClient';
export * from './useLazyQuery';
export * from './useMutation';
export * from './useQuery';
export { useQuery } from './useQuery';
export * from './useSubscription';
export * from './useReactiveVar';
Loading