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

Prevent new data re-render attempts during an existing render #3902

Merged
merged 8 commits into from
Apr 3, 2020
Merged
Show file tree
Hide file tree
Changes from 6 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
5 changes: 5 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Change log

## 3.1.4 (2020-03-27)

- Prevent new data re-render attempts during an existing render. This helps avoid React 16.13.0's "Cannot update a component from inside the function body of a different component" warning (https://github.com/facebook/react/pull/17099). <br/>
[@hwillson](https://github.com/hwillson) in [#3902](https://github.com/apollographql/react-apollo/pull/3902)

## 3.1.3 (2019-10-15)

- Revert the changes made in [#3497](https://github.com/apollographql/react-apollo/pull/3497), which have lead to problems with `onCompleted` being called more often than necessary. <br/>
Expand Down
19,145 changes: 11,523 additions & 7,622 deletions package-lock.json

Large diffs are not rendered by default.

15 changes: 8 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,22 +40,23 @@
"@types/invariant": "2.2.30",
"@types/jest": "24.0.18",
"@types/prop-types": "15.7.3",
"@types/react": "16.9.5",
"@types/react-dom": "16.9.1",
"@types/react": "^16.9.26",
"@types/react-dom": "^16.9.5",
"@types/recompose": "0.30.7",
"@types/zen-observable": "0.8.0",
"apollo-cache": "1.3.2",
"apollo-cache-inmemory": "1.6.3",
"apollo-client": "2.6.4",
"apollo-client": "^2.6.8",
"apollo-link": "1.2.13",
"bundlesize": "0.18.0",
"graphql": "14.5.8",
"graphql-tag": "2.10.1",
"jest": "24.9.0",
"jest-junit": "8.0.0",
"lerna": "3.17.0",
"react": "16.10.2",
"react-dom": "16.10.2",
"prop-types": "^15.7.2",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"recompose": "0.30.0",
"rollup": "1.23.1",
"rollup-plugin-commonjs": "10.1.0",
Expand All @@ -65,7 +66,7 @@
"rollup-plugin-typescript2": "0.24.3",
"ts-jest": "24.1.0",
"tsc-watch": "3.0.2",
"typescript": "3.6.4",
"typescript": "3.8.3",
"zen-observable-ts": "0.8.20"
},
"bundlesize": [
Expand All @@ -92,7 +93,7 @@
{
"name": "@apollo/react-hooks",
"path": "./packages/hooks/lib/react-hooks.cjs.min.js",
"maxSize": "4.2 kB"
"maxSize": "4.3 kB"
},
{
"name": "@apollo/react-ssr",
Expand Down
2 changes: 1 addition & 1 deletion packages/all/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,6 @@
},
"devDependencies": {
"rollup": "1.23.1",
"typescript": "3.6.4"
"typescript": "3.8.3"
}
}
2 changes: 1 addition & 1 deletion packages/common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,6 @@
"jest": "24.9.0",
"rollup": "1.23.1",
"tsc-watch": "3.0.2",
"typescript": "3.6.4"
"typescript": "3.8.3"
}
}
2 changes: 1 addition & 1 deletion packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,6 @@
"jest": "24.9.0",
"rollup": "1.23.1",
"tsc-watch": "3.0.2",
"typescript": "3.6.4"
"typescript": "3.8.3"
}
}
2 changes: 1 addition & 1 deletion packages/hoc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,6 @@
"jest": "24.9.0",
"rollup": "1.23.1",
"tsc-watch": "3.0.2",
"typescript": "3.6.4"
"typescript": "3.8.3"
}
}
2 changes: 1 addition & 1 deletion packages/hooks/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,6 @@
"jest": "24.9.0",
"rollup": "1.23.1",
"tsc-watch": "3.0.2",
"typescript": "3.6.4"
"typescript": "3.8.3"
}
}
62 changes: 58 additions & 4 deletions packages/hooks/src/__tests__/useQuery.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useReducer } from 'react';
import React, { Fragment, useState, useReducer } from 'react';
import { DocumentNode, GraphQLError } from 'graphql';
import gql from 'graphql-tag';
import { MockedProvider, MockLink } from '@apollo/react-testing';
Expand Down Expand Up @@ -121,6 +121,61 @@ describe('useQuery Hook', () => {

await wait();
});

it('should not error when forcing an update with React >= 16.13.0', async () => {
let wasUpdateErrorLogged = false;
const consoleError = console.error;
console.error = (msg: string) => {
console.log(msg);
wasUpdateErrorLogged = msg.indexOf('Cannot update a component') > -1;
};

const CAR_MOCKS = [1, 2, 3, 4, 5, 6].map(something => ({
request: {
query: CAR_QUERY,
variables: { something }
},
result: { data: CAR_RESULT_DATA },
delay: 1000
}));

let renderCount = 0;

const InnerComponent = ({ something }: any) => {
const { loading, data } = useQuery(CAR_QUERY, {
fetchPolicy: 'network-only',
variables: { something }
});
if (loading) return null;
expect(wasUpdateErrorLogged).toBeFalsy();
expect(data).toEqual(CAR_RESULT_DATA);
renderCount += 1;
return null;
};

function WrapperComponent({ something }: any) {
const { loading } = useQuery(CAR_QUERY, {
variables: { something }
});
return loading ? null : <InnerComponent something={something + 1} />;
}

render(
<MockedProvider mocks={CAR_MOCKS}>
<Fragment>
<WrapperComponent something={1} />
<WrapperComponent something={3} />
<WrapperComponent something={5} />
</Fragment>
</MockedProvider>
);

await wait(() => {
expect(renderCount).toBe(3);
}).finally(() => {
console.error = consoleError;
});
});
});

describe('Polling', () => {
Expand Down Expand Up @@ -454,7 +509,7 @@ describe('useQuery Hook', () => {
expect(error).toBeDefined();
expect(error!.message).toEqual('GraphQL error: forced error');
setTimeout(() => {
forceUpdate(0);
forceUpdate();
});
break;
case 2:
Expand Down Expand Up @@ -514,7 +569,7 @@ describe('useQuery Hook', () => {
expect(error).toBeDefined();
expect(error!.message).toEqual('GraphQL error: forced error');
setTimeout(() => {
forceUpdate(0);
forceUpdate();
});
break;
case 2:
Expand Down Expand Up @@ -1094,7 +1149,6 @@ describe('useQuery Hook', () => {
onCompleted(data) {
onCompletedCalled = true;
expect(data).toBeDefined();
console.log(data);
}
});
if (!loading) {
Expand Down
37 changes: 19 additions & 18 deletions packages/hooks/src/data/QueryData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,24 +25,24 @@ import {
import { OperationData } from './OperationData';

export class QueryData<TData, TVariables> extends OperationData {
public onNewData: () => void;

private previousData: QueryPreviousData<TData, TVariables> = {};
private currentObservable: QueryCurrentObservable<TData, TVariables> = {};
private forceUpdate: any;

private runLazy: boolean = false;
private lazyOptions?: QueryLazyOptions<TVariables>;

constructor({
options,
context,
forceUpdate
onNewData
}: {
options: QueryOptions<TData, TVariables>;
context: ApolloContextValue;
forceUpdate: any;
onNewData: () => void;
}) {
super(options, context);
this.forceUpdate = forceUpdate;
this.onNewData = onNewData;
}

public execute(): QueryResult<TData, TVariables> {
Expand Down Expand Up @@ -134,12 +134,15 @@ export class QueryData<TData, TVariables> extends OperationData {
return options;
}

public ssrInitiated() {
return this.context && this.context.renderPromises;
}

private runLazyQuery = (options?: QueryLazyOptions<TVariables>) => {
this.cleanup();

this.runLazy = true;
this.lazyOptions = options;
this.forceUpdate();
this.onNewData();
};

private getExecuteResult = (): QueryResult<TData, TVariables> => {
Expand All @@ -149,7 +152,6 @@ export class QueryData<TData, TVariables> extends OperationData {
};

private getExecuteSsrResult() {
const treeRenderingInitiated = this.context && this.context.renderPromises;
const ssrDisabled = this.getOptions().ssr === false;
const fetchDisabled = this.refreshClient().client.disableNetworkFetches;

Expand All @@ -162,12 +164,12 @@ export class QueryData<TData, TVariables> extends OperationData {

// If SSR has been explicitly disabled, and this function has been called
// on the server side, return the default loading state.
if (ssrDisabled && (treeRenderingInitiated || fetchDisabled)) {
if (ssrDisabled && (this.ssrInitiated() || fetchDisabled)) {
return ssrLoading;
}

let result;
if (treeRenderingInitiated) {
if (this.ssrInitiated()) {
result =
this.context.renderPromises!.addQueryPromise(
this,
Expand All @@ -186,8 +188,7 @@ export class QueryData<TData, TVariables> extends OperationData {
// Set the fetchPolicy to cache-first for network-only and cache-and-network
// fetches for server side renders.
if (
this.context &&
this.context.renderPromises &&
this.ssrInitiated() &&
(options.fetchPolicy === 'network-only' ||
options.fetchPolicy === 'cache-and-network')
) {
Expand All @@ -206,8 +207,8 @@ export class QueryData<TData, TVariables> extends OperationData {
// 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.context && this.context.renderPromises) {
this.currentObservable.query = this.context.renderPromises.getSSRObservable(
if (this.ssrInitiated()) {
this.currentObservable.query = this.context?.renderPromises?.getSSRObservable(
hwillson marked this conversation as resolved.
Show resolved Hide resolved
this.getOptions()
);
}
Expand All @@ -223,8 +224,8 @@ export class QueryData<TData, TVariables> extends OperationData {
...observableQueryOptions
});

if (this.context && this.context.renderPromises) {
this.context.renderPromises.registerSSRObservable(
if (this.ssrInitiated()) {
this.context?.renderPromises?.registerSSRObservable(
this.currentObservable.query,
observableQueryOptions
);
Expand Down Expand Up @@ -279,7 +280,7 @@ export class QueryData<TData, TVariables> extends OperationData {
return;
}

this.forceUpdate();
this.onNewData();
},
error: error => {
this.resubscribeToQuery();
Expand All @@ -291,7 +292,7 @@ export class QueryData<TData, TVariables> extends OperationData {
!isEqual(error, this.previousData.error)
) {
this.previousData.error = error;
this.forceUpdate();
this.onNewData();
}
}
});
Expand Down
47 changes: 41 additions & 6 deletions packages/hooks/src/utils/useBaseQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,39 @@ export function useBaseQuery<TData = any, TVariables = OperationVariables>(
const context = useContext(getApolloContext());
const [tick, forceUpdate] = useReducer(x => x + 1, 0);
const updatedOptions = options ? { ...options, query } : { query };
const isRendering = useRef(true);
hwillson marked this conversation as resolved.
Show resolved Hide resolved
const isRenderScheduled = useRef(false);

const queryDataRef = useRef<QueryData<TData, TVariables>>();

if (!queryDataRef.current) {
queryDataRef.current = new QueryData<TData, TVariables>({
const queryData =
queryDataRef.current ||
new QueryData<TData, TVariables>({
options: updatedOptions as QueryOptions<TData, TVariables>,
context,
forceUpdate
onNewData() {
// 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 in that
// case we'll defer triggering a re-render until we're inside an effect
// hook.
if (!queryData.ssrInitiated() && isRendering.current) {
isRenderScheduled.current = true;
} else {
forceUpdate();
}
}
});
}

const queryData = queryDataRef.current;
queryData.setOptions(updatedOptions);
queryData.context = context;

// SSR won't trigger the effect hook below that stores the current
// `QueryData` instance for future renders, so we'll handle that here if
// the current render is happening server side.
if (queryData.ssrInitiated() && !queryDataRef.current) {
queryDataRef.current = queryData;
}

// `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.
Expand All @@ -51,6 +69,23 @@ export function useBaseQuery<TData = any, TVariables = OperationVariables>(
? (result as QueryTuple<TData, TVariables>)[1]
: (result as QueryResult<TData, TVariables>);

useEffect(() => {
// We only need one instance of the `QueryData` class, so we'll store it
// as a ref to make it available on subsequent renders.
if (!queryDataRef.current) {
queryDataRef.current = queryData;
}

// If `QueryData` requested a re-render to show new data while we were
// in a render phase, let's handle the re-render here where it's safe to do
// so.
isRendering.current = false;
if (isRenderScheduled.current) {
isRenderScheduled.current = false;
forceUpdate();
}
});

useEffect(() => queryData.afterExecute({ lazy }), [
queryResult.loading,
queryResult.networkStatus,
Expand Down
2 changes: 1 addition & 1 deletion packages/ssr/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,6 @@
"jest": "24.9.0",
"rollup": "1.23.1",
"tsc-watch": "3.0.2",
"typescript": "3.6.4"
"typescript": "3.8.3"
}
}
2 changes: 1 addition & 1 deletion packages/testing/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,6 @@
"jest": "24.9.0",
"rollup": "1.23.1",
"tsc-watch": "3.0.2",
"typescript": "3.6.4"
"typescript": "3.8.3"
}
}