From 10b46e529284b84fb559d32b9ed9cd2ad78fffee Mon Sep 17 00:00:00 2001 From: Jim Shepherd Date: Wed, 11 Apr 2018 17:52:42 -0400 Subject: [PATCH 1/6] Add onCompleted prop to Query --- src/Query.tsx | 16 ++++++++-- test/client/Query.test.tsx | 61 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 3 deletions(-) diff --git a/src/Query.tsx b/src/Query.tsx index c47d0ae910..2057ab60de 100644 --- a/src/Query.tsx +++ b/src/Query.tsx @@ -113,6 +113,7 @@ export interface QueryProps { skip?: boolean; client?: ApolloClient; context?: Record; + onCompleted?: (data: TData | {}) => void; } export interface QueryContext { @@ -132,6 +133,7 @@ export default class Query extends children: PropTypes.func.isRequired, fetchPolicy: PropTypes.string, notifyOnNetworkStatusChange: PropTypes.bool, + onCompleted: PropTypes.func, pollInterval: PropTypes.number, query: PropTypes.object.isRequired, variables: PropTypes.object, @@ -172,7 +174,7 @@ export default class Query extends fetchData(): Promise> | boolean { if (this.props.skip) return false; // pull off react options - const { children, ssr, displayName, skip, client, ...opts } = this.props; + const { children, ssr, displayName, skip, client, onCompleted, ...opts } = this.props; let { fetchPolicy } = opts; if (ssr === false) return false; @@ -307,7 +309,7 @@ export default class Query extends private startQuerySubscription = () => { if (this.querySubscription) return; - // store the inital renders worth of result + // store the initial renders worth of result let current: QueryResult | undefined = this.getQueryResult(); this.querySubscription = this.queryObservable!.subscribe({ next: () => { @@ -352,6 +354,14 @@ export default class Query extends } private updateCurrentData = () => { + const { onCompleted } = this.props; + if (onCompleted) { + const currentResult = this.queryObservable!.currentResult(); + const { loading, error, data } = currentResult; + if (!loading && !error) { + onCompleted(data); + } + } // force a rerender that goes through shouldComponentUpdate if (this.hasMounted) this.forceUpdate(); }; @@ -394,7 +404,7 @@ export default class Query extends // If a subscription has not started, then the synchronous call to refetch // must be made at a time when an active network request is being made, so // we ensure that the network requests are deduped, to avoid an - // inconsistant UI state that displays different data for the current query + // inconsistent UI state that displays different data for the current query // alongside a refetched query. // // Once the Query component is mounted and the subscription is made, we diff --git a/test/client/Query.test.tsx b/test/client/Query.test.tsx index c4c76cfbcb..c41cedb49e 100644 --- a/test/client/Query.test.tsx +++ b/test/client/Query.test.tsx @@ -688,6 +688,36 @@ describe('Query component', () => { done(); }); }); + + it('onCompleted with data', done => { + const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } }; + + const mocks = [ + { + request: { query: allPeopleQuery }, + result: { data: data }, + }, + ]; + + const onCompleted = (queryData: Data) => { + expect(stripSymbols(queryData)).toEqual(data); + done(); + }; + + const Component = () => ( + + {() => { + return null; + }} + + ); + + wrapper = mount( + + + , + ); + }); }); describe('props disallow', () => { @@ -737,6 +767,37 @@ describe('Query component', () => { console.error = errorLogger; }); + + it('onCompleted with error', done => { + const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } }; + + const mockError = [ + { + request: { query: allPeopleQuery }, + error: new Error('error occurred'), + }, + ]; + + const onCompleted = jest.fn(); + + const Component = () => ( + + {({ error }) => { + if (error) { + expect(onCompleted).not.toHaveBeenCalled(); + done(); + } + return null; + }} + + ); + + wrapper = mount( + + + , + ); + }); }); describe('should update', () => { From a56942250589873742c32ab956f20523035954bd Mon Sep 17 00:00:00 2001 From: Jim Shepherd Date: Wed, 30 May 2018 15:03:36 -0400 Subject: [PATCH 2/6] MockedProvider addTypename to false for tests --- test/client/Query.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/client/Query.test.tsx b/test/client/Query.test.tsx index 278205a0b1..c83dbe46b7 100644 --- a/test/client/Query.test.tsx +++ b/test/client/Query.test.tsx @@ -713,7 +713,7 @@ describe('Query component', () => { ); wrapper = mount( - + , ); @@ -793,7 +793,7 @@ describe('Query component', () => { ); wrapper = mount( - + , ); From d6ad50518bed36a6e7a18233a9c4d5904741f484 Mon Sep 17 00:00:00 2001 From: Jim Shepherd Date: Wed, 30 May 2018 15:29:47 -0400 Subject: [PATCH 3/6] Add onError prop to complement onCompleted --- src/Query.tsx | 12 ++++--- test/client/Query.test.tsx | 69 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 74 insertions(+), 7 deletions(-) diff --git a/src/Query.tsx b/src/Query.tsx index 2057ab60de..9ba6f72cfd 100644 --- a/src/Query.tsx +++ b/src/Query.tsx @@ -114,6 +114,7 @@ export interface QueryProps { client?: ApolloClient; context?: Record; onCompleted?: (data: TData | {}) => void; + onError?: (error: ApolloError) => void; } export interface QueryContext { @@ -134,6 +135,7 @@ export default class Query extends fetchPolicy: PropTypes.string, notifyOnNetworkStatusChange: PropTypes.bool, onCompleted: PropTypes.func, + onError: PropTypes.func, pollInterval: PropTypes.number, query: PropTypes.object.isRequired, variables: PropTypes.object, @@ -174,7 +176,7 @@ export default class Query extends fetchData(): Promise> | boolean { if (this.props.skip) return false; // pull off react options - const { children, ssr, displayName, skip, client, onCompleted, ...opts } = this.props; + const { children, ssr, displayName, skip, client, onCompleted, onError, ...opts } = this.props; let { fetchPolicy } = opts; if (ssr === false) return false; @@ -354,12 +356,14 @@ export default class Query extends } private updateCurrentData = () => { - const { onCompleted } = this.props; - if (onCompleted) { + const { onCompleted, onError } = this.props; + if (onCompleted || onError) { const currentResult = this.queryObservable!.currentResult(); const { loading, error, data } = currentResult; - if (!loading && !error) { + if (onCompleted && !loading && !error) { onCompleted(data); + } else if (onError && !loading && error) { + onError(error); } } // force a rerender that goes through shouldComponentUpdate diff --git a/test/client/Query.test.tsx b/test/client/Query.test.tsx index c83dbe46b7..eb37cee2a0 100644 --- a/test/client/Query.test.tsx +++ b/test/client/Query.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import ApolloClient, { NetworkStatus } from 'apollo-client'; +import ApolloClient, { ApolloError, NetworkStatus } from 'apollo-client'; import { mount, ReactWrapper } from 'enzyme'; import { InMemoryCache as Cache } from 'apollo-cache-inmemory'; import { ApolloProvider, Query } from '../../src'; @@ -718,6 +718,42 @@ describe('Query component', () => { , ); }); + + it('onError with data', done => { + const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } }; + + const mocks = [ + { + request: { query: allPeopleQuery }, + result: { data: data }, + }, + ]; + + const onError = (queryError: ApolloError) => { + expect(queryError).toEqual(null); + done(); + }; + + const onError = jest.fn(); + + const Component = () => ( + + {({ loading }) => { + if (!loading) { + expect(onError).not.toHaveBeenCalled(); + done(); + } + return null; + }} + + ); + + wrapper = mount( + + + , + ); + }); }); describe('props disallow', () => { @@ -769,8 +805,6 @@ describe('Query component', () => { }); it('onCompleted with error', done => { - const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } }; - const mockError = [ { request: { query: allPeopleQuery }, @@ -798,6 +832,35 @@ describe('Query component', () => { , ); }); + + it('onError with error', done => { + const error = new Error('error occurred'); + const mockError = [ + { + request: { query: allPeopleQuery }, + error: error, + }, + ]; + + const onError = (queryError: ApolloError) => { + expect(queryError.networkError).toEqual(error); + done(); + }; + + const Component = () => ( + + {() => { + return null; + }} + + ); + + wrapper = mount( + + + , + ); + }); }); describe('should update', () => { From abbd580c94daa6fdc478cab1ebaf591a01884383 Mon Sep 17 00:00:00 2001 From: Jim Shepherd Date: Wed, 30 May 2018 15:39:17 -0400 Subject: [PATCH 4/6] Rename onError function in tests to avoid name dupe --- test/client/Query.test.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/client/Query.test.tsx b/test/client/Query.test.tsx index eb37cee2a0..3a9b8deceb 100644 --- a/test/client/Query.test.tsx +++ b/test/client/Query.test.tsx @@ -729,7 +729,7 @@ describe('Query component', () => { }, ]; - const onError = (queryError: ApolloError) => { + const onErrorFunc = (queryError: ApolloError) => { expect(queryError).toEqual(null); done(); }; @@ -737,7 +737,7 @@ describe('Query component', () => { const onError = jest.fn(); const Component = () => ( - + {({ loading }) => { if (!loading) { expect(onError).not.toHaveBeenCalled(); @@ -842,13 +842,13 @@ describe('Query component', () => { }, ]; - const onError = (queryError: ApolloError) => { + const onErrorFunc = (queryError: ApolloError) => { expect(queryError.networkError).toEqual(error); done(); }; const Component = () => ( - + {() => { return null; }} From 01b20fa1afb90c27d53b8fdec6e200eacde37af1 Mon Sep 17 00:00:00 2001 From: Jim Shepherd Date: Fri, 29 Jun 2018 14:00:22 -0400 Subject: [PATCH 5/6] Fix typescript error for onCompleted test --- test/client/Query.test.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/test/client/Query.test.tsx b/test/client/Query.test.tsx index 3a9b8deceb..a0939c625f 100644 --- a/test/client/Query.test.tsx +++ b/test/client/Query.test.tsx @@ -690,17 +690,15 @@ describe('Query component', () => { }); it('onCompleted with data', done => { - const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } }; - const mocks = [ { request: { query: allPeopleQuery }, - result: { data: data }, + result: { data: allPeopleData }, }, ]; - const onCompleted = (queryData: Data) => { - expect(stripSymbols(queryData)).toEqual(data); + const onCompleted = (queryData: Data | {}) => { + expect(stripSymbols(queryData)).toEqual(allPeopleData); done(); }; From 198cc9dbc5d2b3eb6b2dd521d9ad49f65eef4103 Mon Sep 17 00:00:00 2001 From: Hugh Willson Date: Fri, 29 Jun 2018 15:38:58 -0400 Subject: [PATCH 6/6] Changelog update; slight code formatting --- Changelog.md | 9 ++++++++- src/Query.tsx | 2 ++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index 9d5c41b943..fb90a2c15d 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,6 +1,13 @@ # Change log -## 2.1.8 (Jun3 28, 2018) +## vNext + +- Added `onCompleted` and `onError` props to the `Query` component, than can + be used to register callback functions that are to be executed after a + query successfully completes, or an error occurs. + [@jeshep](https://github.com/jeshep) in [#1922](https://github.com/apollographql/react-apollo/pull/1922) + +## 2.1.8 (June 28, 2018) - Addressed deployment issue. diff --git a/src/Query.tsx b/src/Query.tsx index a33d99f4fd..773951b7d3 100644 --- a/src/Query.tsx +++ b/src/Query.tsx @@ -175,6 +175,7 @@ export default class Query extends // For server-side rendering (see getDataFromTree.ts) fetchData(): Promise> | boolean { if (this.props.skip) return false; + // pull off react options const { children, ssr, displayName, skip, client, onCompleted, onError, ...opts } = this.props; @@ -366,6 +367,7 @@ export default class Query extends onError(error); } } + // force a rerender that goes through shouldComponentUpdate if (this.hasMounted) this.forceUpdate(); };