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

Drastically simplify useQuery default options processing #9665

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## Apollo Client 3.6.3 (unreleased)

### Bug Fixes

- Simplify `useQuery(query, { defaultOptions })` default options processing in order to fix bug where `skip: true` queries failed to execute upon switching to `skip: false`. <br/>
[@benjamn](https://github.com/benjamn) in [#9665](https://github.com/apollographql/apollo-client/pull/9665)

## Apollo Client 3.6.2 (2022-05-02)

### Bug Fixes
Expand Down
123 changes: 39 additions & 84 deletions src/react/hooks/useQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,10 +204,39 @@ class InternalState<TData, TVariables> {
private useOptions(
options: QueryHookOptions<TData, TVariables>,
) {
const prevQueryHookOptions = this.queryHookOptions;
const watchQueryOptions = this.createWatchQueryOptions(
this.queryHookOptions = options,
);

if (options.skip) {
const {
fetchPolicy,
initialFetchPolicy = fetchPolicy,
} = watchQueryOptions;

// When skipping, we set watchQueryOptions.fetchPolicy initially to
// "standby", but we also need/want to preserve the initial non-standby
// fetchPolicy that would have been used if not skipping.
Object.assign(watchQueryOptions, {
initialFetchPolicy,
fetchPolicy: 'standby',
});

} else if (
prevQueryHookOptions &&
prevQueryHookOptions.skip &&
!watchQueryOptions.fetchPolicy
) {
// When we're coming out of skip/standby mode, we have to provide a truthy
// fetchPolicy to override fetchPolicy:"standby", rather than leaving
// watchQueryOptions.fetchPolicy undefined.
watchQueryOptions.fetchPolicy =
options.defaultOptions?.fetchPolicy ||
this.client.defaultOptions.watchQuery?.fetchPolicy ||
'cache-first';
}

// Update this.watchQueryOptions, but only when they have changed, which
// allows us to depend on the referential stability of
// this.watchQueryOptions elsewhere.
Expand Down Expand Up @@ -295,65 +324,10 @@ class InternalState<TData, TVariables> {
// query property that we add below.
...otherOptions
}: QueryHookOptions<TData, TVariables> = {}): WatchQueryOptions<TVariables, TData> {
// We use the mergeOptions helper function (which uses compact(...) and
// shallow-merges variables) to combine globalDefaults with any local
// defaultOptions provided to useQuery.
const toMerge: Partial<WatchQueryOptions<TVariables, TData>>[] = [];

// Merge global client.watchQuery default options with the lowest priority.
const globalDefaults = this.client.defaultOptions.watchQuery;
if (globalDefaults) toMerge.push(globalDefaults);

// Next, merge any defaultOptions passed directly to useQuery.
if (defaultOptions) toMerge.push(defaultOptions);

const latestOptions = this.observable && this.observable.options;
if (latestOptions && toMerge.length) {
// If we already have this.watchQueryOptions, those options should take
// precedence over default options of the same name. It might be simpler
// to do toMerge.push(this.watchQueryOptions), but that potentially
// (re)injects unrelated/unwanted options. Passing Object.create(null) as
// the second argument to toMerge.reduce ensures the result is a newly
// created object, so we can safely modify it in the forEach loop below.
const defaults = toMerge.reduce(mergeOptions, Object.create(null));

// Compact the toMerge array to hold only the merged defaults. This is
// equivalent to toMerge.splice(0, toMerge.length, defaults).
toMerge.length = 1;
toMerge[0] = defaults;

Object.keys(defaults).forEach(
(defaultOptionName: keyof WatchQueryOptions<TVariables, TData>) => {
const currentOptionValue = latestOptions[defaultOptionName];
if (
hasOwnProperty.call(latestOptions, defaultOptionName) &&
!equal(defaults[defaultOptionName], currentOptionValue)
) {
// If you keep passing useQuery({ defaultOptions: { variables }}),
// those default variables continue to provide their default values
// every time, though in most cases this.watchQueryOptions.variables
// will have a current value for every default variable name, so the
// defaults don't matter. However, if a variable has been removed
// from this.watchQueryOptions.variables, future useQuery calls can
// restore its default value from defaultOptions.variables.
defaults[defaultOptionName] = defaultOptionName === "variables"
? { ...defaults.variables, ...currentOptionValue }
: currentOptionValue;
}
},
);
}

// Give highest precedence to any non-default WatchQueryOptions passed
// directly to useQuery.
toMerge.push(otherOptions);

const merged = toMerge.reduce(mergeOptions, Object.create(null));

// This Object.assign is safe because merged is the fresh object created by
// the Object.create(null) argument to toMerge.reduce.
// This Object.assign is safe because otherOptions is a fresh ...rest object
// that did not exist until just now, so modifications are still allowed.
const watchQueryOptions: WatchQueryOptions<TVariables, TData> =
Object.assign(merged, { query: this.query });
Object.assign(otherOptions, { query: this.query });

if (
this.renderPromises &&
Expand All @@ -365,29 +339,6 @@ class InternalState<TData, TVariables> {
// this behavior was added to react-apollo without explanation in this PR
// https://github.com/apollographql/react-apollo/pull/1579
watchQueryOptions.fetchPolicy = 'cache-first';
} else if (!watchQueryOptions.fetchPolicy) {
// We applied all available fetchPolicy default values above (from
// globalDefaults and defaultOptions), so, if fetchPolicy is still
// undefined, fall back to the default default (no typo), cache-first.
watchQueryOptions.fetchPolicy = 'cache-first';
}

if (skip) {
const {
// The watchQueryOptions.initialFetchPolicy field usually defaults to
// watchQueryOptions.fetchPolicy, which has now been properly
// defaulted/initialized. However, watchQueryOptions.initialFetchPolicy
// can be provided explicitly instead, if more control is desired.
initialFetchPolicy = watchQueryOptions.fetchPolicy,
} = watchQueryOptions;

// When skipping, we set watchQueryOptions.fetchPolicy initially to
// "standby", but we also need/want to preserve the initial non-standby
// fetchPolicy that would have been used if not skipping.
Object.assign(watchQueryOptions, {
initialFetchPolicy,
fetchPolicy: 'standby',
});
}

if (!watchQueryOptions.variables) {
Expand Down Expand Up @@ -417,9 +368,13 @@ class InternalState<TData, TVariables> {
this.renderPromises
&& this.renderPromises.getSSRObservable(this.watchQueryOptions)
|| this.observable // Reuse this.observable if possible (and not SSR)
|| this.client.watchQuery({
...this.watchQueryOptions,
});
|| this.client.watchQuery(mergeOptions(
// Any options.defaultOptions passed to useQuery serve as default
// options because we use them only here, when first creating the
// ObservableQuery by calling client.watchQuery.
this.queryHookOptions.defaultOptions,
this.watchQueryOptions,
));
Comment on lines +371 to +377
Copy link
Member Author

Choose a reason for hiding this comment

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

This is the crux of the new strategy, allowing all the other simplifications.


this.obsQueryFields = useMemo(() => ({
refetch: obsQuery.refetch.bind(obsQuery),
Expand Down
4 changes: 2 additions & 2 deletions src/utilities/common/mergeOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ type OptionsUnion<TData, TVariables, TContext> =
export function mergeOptions<
TOptions extends OptionsUnion<any, any, any>
>(
defaults: TOptions | Partial<TOptions>,
defaults: TOptions | Partial<TOptions> | undefined,
options: TOptions | Partial<TOptions>,
): TOptions {
return compact(defaults, options, options.variables && {
variables: {
...defaults.variables,
...(defaults && defaults.variables),
...options.variables,
},
});
Expand Down