Skip to content
This repository was archived by the owner on Apr 13, 2023. It is now read-only.

Commit 2b7073b

Browse files
jasonpauloshwillson
authored andcommitted
Fix component stuck in loading state for network-only fetch policy (#3126)
* Add ability for Query to detect when lastResult is not accurate * Add regression test for #2899 * Streamline interactions with Apollo Client This commit streamlines some of the React Apollo <--> Apollo Client communication points, to help reduce temporary placeholders and variables used by React Apollo to control rendering. The current data result that is to be rendered now comes from only one place, the initialized `ObservableQuery` instance. * Code review tweaks * Changelog update
1 parent dca8f7a commit 2b7073b

File tree

6 files changed

+168
-96
lines changed

6 files changed

+168
-96
lines changed

Changelog.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,11 @@
3131
responses couldn't be handled). The `Query` component can now handle an
3232
error in a response, then continue to handle a valid response afterwards. <br/>
3333
[@hwillson](https://github.com/hwillson) in [#3107](https://github.com/apollographql/react-apollo/pull/3107)
34-
- Reorder `Subscription` component code to avoid setting state on unmounted
34+
- Reorder `Subscription` component code to avoid setting state on unmounted
3535
component. <br/>
3636
[@jasonpaulos](https://github.com/jasonpaulos) in [#3139](https://github.com/apollographql/react-apollo/pull/3139)
37+
- Fix component stuck in `loading` state for `network-only` fetch policy. <br/>
38+
[@jasonpaulos](https://github.com/jasonpaulos) in [#3126](https://github.com/apollographql/react-apollo/pull/3126)
3739

3840

3941
## 2.5.6 (2019-05-22)

src/Query.tsx

+36-55
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import ApolloClient, {
77
NetworkStatus,
88
FetchMoreOptions,
99
FetchMoreQueryOptions,
10+
ApolloCurrentResult
1011
} from 'apollo-client';
1112
import { DocumentNode } from 'graphql';
1213
import { ZenObservable } from 'zen-observable-ts';
@@ -115,7 +116,6 @@ export default class Query<TData = any, TVariables = OperationVariables> extends
115116
// only delete queryObservable when we unmount the component.
116117
private queryObservable?: ObservableQuery<TData, TVariables> | null;
117118
private querySubscription?: ZenObservable.Subscription;
118-
private previousData: any = {};
119119
private refetcherQueue?: {
120120
args: any;
121121
resolve: (value?: any | PromiseLike<any>) => void;
@@ -124,7 +124,7 @@ export default class Query<TData = any, TVariables = OperationVariables> extends
124124

125125
private hasMounted: boolean = false;
126126
private operation?: IDocumentDefinition;
127-
private lastResult: ApolloQueryResult<TData> | null = null;
127+
private lastRenderedResult: ApolloQueryResult<TData> | null = null;
128128

129129
constructor(props: QueryProps<TData, TVariables>, context: QueryContext) {
130130
super(props, context);
@@ -175,7 +175,7 @@ export default class Query<TData = any, TVariables = OperationVariables> extends
175175
this.hasMounted = true;
176176
if (this.props.skip) return;
177177

178-
this.startQuerySubscription(true);
178+
this.startQuerySubscription();
179179
if (this.refetcherQueue) {
180180
const { args, resolve, reject } = this.refetcherQueue;
181181
this.queryObservable!.refetch(args)
@@ -187,6 +187,7 @@ export default class Query<TData = any, TVariables = OperationVariables> extends
187187
componentWillReceiveProps(nextProps: QueryProps<TData, TVariables>, nextContext: QueryContext) {
188188
// the next render wants to skip
189189
if (nextProps.skip && !this.props.skip) {
190+
this.queryObservable!.resetLastResults();
190191
this.removeQuerySubscription();
191192
return;
192193
}
@@ -201,11 +202,10 @@ export default class Query<TData = any, TVariables = OperationVariables> extends
201202
this.client = nextClient;
202203
this.removeQuerySubscription();
203204
this.queryObservable = null;
204-
this.previousData = {};
205-
this.updateQuery(nextProps);
206205
}
207206

208207
if (this.props.query !== nextProps.query) {
208+
this.queryObservable!.resetLastResults();
209209
this.removeQuerySubscription();
210210
}
211211

@@ -300,52 +300,28 @@ export default class Query<TData = any, TVariables = OperationVariables> extends
300300
.catch(() => null);
301301
}
302302

303-
private startQuerySubscription = (justMounted: boolean = false) => {
303+
private startQuerySubscription = () => {
304304
// When the `Query` component receives new props, or when we explicitly
305305
// re-subscribe to a query using `resubscribeToQuery`, we start a new
306306
// subscription in this method. To avoid un-necessary re-renders when
307307
// receiving new props or re-subscribing, we track the full last
308308
// observable result so it can be compared against incoming new data.
309309
// We only trigger a re-render if the incoming result is different than
310-
// the stored `lastResult`.
311-
//
312-
// It's important to note that when a component is first mounted,
313-
// the `startQuerySubscription` method is also triggered. During a first
314-
// mount, we don't want to store or use the last result, as we always
315-
// need the first render to happen, even if there was a previous last
316-
// result (which can happen when the same component is mounted, unmounted,
317-
// and mounted again).
318-
if (!justMounted) {
319-
this.lastResult = this.queryObservable!.getLastResult();
320-
}
310+
// the stored `lastRenderedResult`.
321311

322312
if (this.querySubscription) return;
323313

324-
// store the initial renders worth of result
325-
let initial: QueryResult<TData, TVariables> | undefined = this.getQueryResult();
326-
327314
this.querySubscription = this.queryObservable!.subscribe({
328-
next: ({ loading, networkStatus, data }) => {
329-
// to prevent a quick second render from the subscriber
330-
// we compare to see if the original started finished (from cache) and is unchanged
331-
if (initial && initial.networkStatus === 7 && shallowEqual(initial.data, data)) {
332-
initial = undefined;
333-
return;
334-
}
335-
315+
next: (result) => {
336316
if (
337-
this.lastResult &&
338-
this.lastResult.loading === loading &&
339-
this.lastResult.networkStatus === networkStatus &&
340-
shallowEqual(this.lastResult.data, data)
317+
this.lastRenderedResult &&
318+
this.lastRenderedResult.loading === result.loading &&
319+
this.lastRenderedResult.networkStatus === result.networkStatus &&
320+
shallowEqual(this.lastRenderedResult.data, result.data)
341321
) {
342322
return;
343323
}
344324

345-
initial = undefined;
346-
if (this.lastResult) {
347-
this.lastResult = this.queryObservable!.getLastResult();
348-
}
349325
this.updateCurrentData();
350326
},
351327
error: error => {
@@ -359,8 +335,8 @@ export default class Query<TData = any, TVariables = OperationVariables> extends
359335

360336
private removeQuerySubscription = () => {
361337
if (this.querySubscription) {
362-
this.lastResult = this.queryObservable!.getLastResult();
363338
this.querySubscription.unsubscribe();
339+
delete this.lastRenderedResult;
364340
delete this.querySubscription;
365341
}
366342
};
@@ -403,22 +379,21 @@ export default class Query<TData = any, TVariables = OperationVariables> extends
403379
}
404380

405381
private getQueryResult = (): QueryResult<TData, TVariables> => {
406-
let data = { data: Object.create(null) as TData } as any;
382+
let result = { data: Object.create(null) as TData } as any;
407383
// Attach bound methods
408-
Object.assign(data, observableQueryFields(this.queryObservable!));
384+
Object.assign(result, observableQueryFields(this.queryObservable!));
409385

410386
// When skipping a query (ie. we're not querying for data but still want
411387
// to render children), make sure the `data` is cleared out and
412388
// `loading` is set to `false` (since we aren't loading anything).
413389
if (this.props.skip) {
414-
data = {
415-
...data,
390+
result = {
391+
...result,
416392
data: undefined,
417393
error: undefined,
418394
loading: false,
419395
};
420396
} else {
421-
// Fetch the current result (if any) from the store.
422397
const currentResult = this.queryObservable!.currentResult();
423398
const { loading, partial, networkStatus, errors } = currentResult;
424399
let { error } = currentResult;
@@ -430,12 +405,15 @@ export default class Query<TData = any, TVariables = OperationVariables> extends
430405
}
431406

432407
const { fetchPolicy } = this.queryObservable!.options;
433-
Object.assign(data, { loading, networkStatus, error });
408+
Object.assign(result, { loading, networkStatus, error });
409+
410+
const previousData =
411+
this.lastRenderedResult ? this.lastRenderedResult.data : {};
434412

435413
if (loading) {
436-
Object.assign(data.data, this.previousData, currentResult.data);
414+
Object.assign(result.data, previousData, currentResult.data);
437415
} else if (error) {
438-
Object.assign(data, {
416+
Object.assign(result, {
439417
data: (this.queryObservable!.getLastResult() || {}).data,
440418
});
441419
} else if (
@@ -444,11 +422,13 @@ export default class Query<TData = any, TVariables = OperationVariables> extends
444422
) {
445423
// Make sure data pulled in by a `no-cache` query is preserved
446424
// when the components parent tree is re-rendered.
447-
data.data = this.previousData;
425+
result.data = previousData;
448426
} else {
449427
const { partialRefetch } = this.props;
450428
if (
451429
partialRefetch &&
430+
currentResult.data !== null &&
431+
typeof currentResult.data === 'object' &&
452432
Object.keys(currentResult.data).length === 0 &&
453433
partial &&
454434
fetchPolicy !== 'cache-only'
@@ -461,13 +441,13 @@ export default class Query<TData = any, TVariables = OperationVariables> extends
461441
// the original `Query` component are expecting certain data values to
462442
// exist, and they're all of a sudden stripped away. To help avoid
463443
// this we'll attempt to refetch the `Query` data.
464-
Object.assign(data, { loading: true, networkStatus: NetworkStatus.loading });
465-
data.refetch();
466-
return data;
444+
Object.assign(result, { loading: true, networkStatus: NetworkStatus.loading });
445+
result.refetch();
446+
this.lastRenderedResult = result;
447+
return result;
467448
}
468449

469-
Object.assign(data.data, currentResult.data);
470-
this.previousData = currentResult.data;
450+
Object.assign(result.data, currentResult.data);
471451
}
472452
}
473453

@@ -491,9 +471,9 @@ export default class Query<TData = any, TVariables = OperationVariables> extends
491471
// always hit the network with refetch, since the components data will be
492472
// updated and a network request is not currently active.
493473
if (!this.querySubscription) {
494-
const oldRefetch = (data as QueryControls<TData, TVariables>).refetch;
474+
const oldRefetch = (result as QueryControls<TData, TVariables>).refetch;
495475

496-
(data as QueryControls<TData, TVariables>).refetch = args => {
476+
(result as QueryControls<TData, TVariables>).refetch = args => {
497477
if (this.querySubscription) {
498478
return oldRefetch(args);
499479
} else {
@@ -512,7 +492,8 @@ export default class Query<TData = any, TVariables = OperationVariables> extends
512492
this.queryObservable!.resetQueryStoreErrors();
513493
});
514494

515-
data.client = this.client;
516-
return data;
495+
result.client = this.client;
496+
this.lastRenderedResult = result;
497+
return result;
517498
};
518499
}

test/client/Query.test.tsx

+18-10
Original file line numberDiff line numberDiff line change
@@ -524,14 +524,18 @@ describe('Query component', () => {
524524

525525
describe('props allow', () => {
526526
it('custom fetch-policy', done => {
527+
let count = 0;
527528
const Component = () => (
528529
<Query query={allPeopleQuery} fetchPolicy={'cache-only'}>
529530
{result => {
530-
catchAsyncError(done, () => {
531-
expect(result.loading).toBeFalsy();
532-
expect(result.networkStatus).toBe(NetworkStatus.ready);
533-
done();
534-
});
531+
if (count === 0) {
532+
catchAsyncError(done, () => {
533+
expect(result.loading).toBeFalsy();
534+
expect(result.networkStatus).toBe(NetworkStatus.ready);
535+
done();
536+
});
537+
}
538+
count += 1;
535539
return null;
536540
}}
537541
</Query>
@@ -545,14 +549,18 @@ describe('Query component', () => {
545549
});
546550

547551
it('default fetch-policy', done => {
552+
let count = 0;
548553
const Component = () => (
549554
<Query query={allPeopleQuery}>
550555
{result => {
551-
catchAsyncError(done, () => {
552-
expect(result.loading).toBeFalsy();
553-
expect(result.networkStatus).toBe(NetworkStatus.ready);
554-
done();
555-
});
556+
if (count === 0) {
557+
catchAsyncError(done, () => {
558+
expect(result.loading).toBeFalsy();
559+
expect(result.networkStatus).toBe(NetworkStatus.ready);
560+
done();
561+
});
562+
}
563+
count += 1;
556564
return null;
557565
}}
558566
</Query>

test/client/graphql/mutations/recycled-queries.test.tsx

+4-18
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ describe('graphql(mutation) update queries', () => {
128128

129129
render() {
130130
try {
131-
switch (queryRenderCount++) {
131+
switch (queryRenderCount) {
132132
case 0:
133133
expect(this.props.data!.loading).toBeTruthy();
134134
expect(this.props.data!.todo_list).toBeFalsy();
@@ -141,21 +141,6 @@ describe('graphql(mutation) update queries', () => {
141141
});
142142
break;
143143
case 2:
144-
expect(queryMountCount).toBe(1);
145-
expect(queryUnmountCount).toBe(0);
146-
expect(stripSymbols(this.props.data!.todo_list)).toEqual({
147-
id: '123',
148-
title: 'how to apollo',
149-
tasks: [
150-
{
151-
id: '99',
152-
text: 'This one was created with a mutation.',
153-
completed: true,
154-
},
155-
],
156-
});
157-
break;
158-
case 3:
159144
expect(queryMountCount).toBe(2);
160145
expect(queryUnmountCount).toBe(1);
161146
expect(stripSymbols(this.props.data!.todo_list)).toEqual({
@@ -175,7 +160,7 @@ describe('graphql(mutation) update queries', () => {
175160
],
176161
});
177162
break;
178-
case 4:
163+
case 3:
179164
expect(stripSymbols(this.props.data!.todo_list)).toEqual({
180165
id: '123',
181166
title: 'how to apollo',
@@ -196,6 +181,7 @@ describe('graphql(mutation) update queries', () => {
196181
default:
197182
throw new Error('Rendered too many times');
198183
}
184+
queryRenderCount += 1;
199185
} catch (error) {
200186
reject(error);
201187
}
@@ -247,7 +233,7 @@ describe('graphql(mutation) update queries', () => {
247233
expect(todoUpdateQueryCount).toBe(2);
248234
expect(queryMountCount).toBe(2);
249235
expect(queryUnmountCount).toBe(2);
250-
expect(queryRenderCount).toBe(4);
236+
expect(queryRenderCount).toBe(3);
251237
resolve();
252238
} catch (error) {
253239
reject(error);

test/client/graphql/queries/lifecycle.test.tsx

-12
Original file line numberDiff line numberDiff line change
@@ -581,20 +581,8 @@ describe('[queries] lifecycle', () => {
581581
{ loading: false, a: 7, b: 8, c: 9 },
582582

583583
// Load 6
584-
585-
// The first render is caused by the component having its state updated
586-
// when switching the client. The 2nd and 3rd renders are caused by the
587-
// component re-subscribing to get data from Apollo Client.
588-
{ loading: false, a: 1, b: 2, c: 3 },
589-
{ loading: false, a: 1, b: 2, c: 3 },
590584
{ loading: false, a: 1, b: 2, c: 3 },
591-
592-
{ loading: false, a: 4, b: 5, c: 6 },
593585
{ loading: false, a: 4, b: 5, c: 6 },
594-
{ loading: false, a: 4, b: 5, c: 6 },
595-
596-
{ loading: false, a: 7, b: 8, c: 9 },
597-
{ loading: false, a: 7, b: 8, c: 9 },
598586
{ loading: false, a: 7, b: 8, c: 9 },
599587
]);
600588
});

0 commit comments

Comments
 (0)