From 1ae46f11c31d74d3e70ca00a4e52fdeb7552ef80 Mon Sep 17 00:00:00 2001 From: Dirk-Jan Rutten Date: Tue, 26 Dec 2017 16:20:48 +0100 Subject: [PATCH] Implemented the ApolloConsumer component. (#1399) * Implemented the ApolloConsumer component. * Wrapped the withApollo component in the ApolloConsumer component. * Updated the changelog * Removed render prop in favour of children render prop --- Changelog.md | 3 +- examples/typescript/package.json | 6 +- src/ApolloConsumer.tsx | 26 ++++++++ src/Query.tsx | 7 ++- src/getDataFromTree.ts | 4 +- src/graphql.tsx | 5 +- src/withApollo.tsx | 42 ++++++------- test/flow-usage.js | 33 +++++----- test/react-native/component.test.tsx | 12 ++-- test/react-web/client/ApolloConsumer.test.js | 57 ++++++++++++++++++ .../client/graphql/queries/index.test.tsx | 28 ++++----- .../client/graphql/subscriptions.test.tsx | 30 +++++----- .../react-web/server/getDataFromTree.test.tsx | 60 +++++++++---------- 13 files changed, 196 insertions(+), 117 deletions(-) create mode 100644 src/ApolloConsumer.tsx create mode 100644 test/react-web/client/ApolloConsumer.test.js diff --git a/Changelog.md b/Changelog.md index a0d9c54611..c804e85a4d 100644 --- a/Changelog.md +++ b/Changelog.md @@ -11,8 +11,9 @@ a) full typing; and b) ease of use; and c) consistency. New parameterized is: first three params (`TChildProps` can be derived). [#1402](https://github.com/apollographql/react-apollo/pull/1402) - Typescript - fix `graphql` HOC inference [#1402](https://github.com/apollographql/react-apollo/pull/1402) - **Remove deprecated** `operationOptions.options.skip`, use `operationOptions.skip` instead -- Added component [#1399](https://github.com/apollographql/react-apollo/pull/1398) +- Added component [#1398](https://github.com/apollographql/react-apollo/pull/1398) - Made prettier solely responsible for formatting, removed all formatting linting rules from tslint [#1452](https://github.com/apollographql/react-apollo/pull/1452) +- add component [#1399](https://github.com/apollographql/react-apollo/pull/1399) ### 2.0.4 - rolled back on the lodash-es changes from diff --git a/examples/typescript/package.json b/examples/typescript/package.json index e5f942cc6e..d5b5a98878 100644 --- a/examples/typescript/package.json +++ b/examples/typescript/package.json @@ -23,7 +23,9 @@ "build": "react-scripts-ts build", "test": "react-scripts-ts test --env=jsdom", "eject": "react-scripts-ts eject", - "schema": "apollo-codegen introspect-schema https://mpjk0plp9.lp.gql.zone/graphql --output ./src/schema.json", - "types": "apollo-codegen generate ./src/**/*.tsx --schema ./src/schema.json --target typescript --output ./src/schema.ts --add-typename" + "schema": + "apollo-codegen introspect-schema https://mpjk0plp9.lp.gql.zone/graphql --output ./src/schema.json", + "types": + "apollo-codegen generate ./src/**/*.tsx --schema ./src/schema.json --target typescript --output ./src/schema.ts --add-typename" } } diff --git a/src/ApolloConsumer.tsx b/src/ApolloConsumer.tsx new file mode 100644 index 0000000000..3b3e3f66c8 --- /dev/null +++ b/src/ApolloConsumer.tsx @@ -0,0 +1,26 @@ +import * as React from 'react'; +import * as PropTypes from 'prop-types'; +import ApolloClient from 'apollo-client'; +const invariant = require('invariant'); + +export interface ApolloConsumerProps { + children: (client: ApolloClient) => React.ReactElement; +} + +const ApolloConsumer: React.StatelessComponent = ( + props, + context, +) => { + invariant( + !!context.client, + `Could not find "client" in the context of ApolloConsumer. Wrap the root component in an `, + ); + + return props.children(context.client); +}; + +ApolloConsumer.contextTypes = { + client: PropTypes.object.isRequired, +}; + +export default ApolloConsumer; diff --git a/src/Query.tsx b/src/Query.tsx index 0dededd357..6eeddfa363 100644 --- a/src/Query.tsx +++ b/src/Query.tsx @@ -136,9 +136,10 @@ class Query extends React.Component { invariant( operation.type === DocumentType.Query, - `The component requires a graphql query, but got a ${ - operation.type === DocumentType.Mutation ? 'mutation' : 'subscription' - }.`, + `The component requires a graphql query, but got a ${operation.type === + DocumentType.Mutation + ? 'mutation' + : 'subscription'}.`, ); const clientOptions = { diff --git a/src/getDataFromTree.ts b/src/getDataFromTree.ts index a87de32465..89b8a39f78 100755 --- a/src/getDataFromTree.ts +++ b/src/getDataFromTree.ts @@ -170,9 +170,7 @@ export function getDataFromTree( errors.length === 1 ? errors[0] : new Error( - `${ - errors.length - } errors were thrown when executing your GraphQL queries.`, + `${errors.length} errors were thrown when executing your GraphQL queries.`, ); error.queryErrors = errors; throw error; diff --git a/src/graphql.tsx b/src/graphql.tsx index 1b026d0701..5b458f2b7c 100644 --- a/src/graphql.tsx +++ b/src/graphql.tsx @@ -303,9 +303,8 @@ export default function graphql< `The operation '${operation.name}' wrapping '${getDisplayName( WrappedComponent, )}' ` + - `is expecting a variable: '${ - variable.name.value - }' but it was not found in the props ` + + `is expecting a variable: '${variable.name + .value}' but it was not found in the props ` + `passed to '${graphQLDisplayName}'`, ); } diff --git a/src/withApollo.tsx b/src/withApollo.tsx index 06ff52e094..ec3a3eb0b8 100644 --- a/src/withApollo.tsx +++ b/src/withApollo.tsx @@ -1,14 +1,11 @@ -import { Component, createElement } from 'react'; -import * as PropTypes from 'prop-types'; +import * as React from 'react'; const invariant = require('invariant'); -const assign = require('object-assign'); const hoistNonReactStatics = require('hoist-non-react-statics'); -import ApolloClient from 'apollo-client'; - import { OperationOption } from './types'; +import ApolloConsumer from './ApolloConsumer'; function getDisplayName(WrappedComponent) { return WrappedComponent.displayName || WrappedComponent.name || 'Component'; @@ -20,28 +17,16 @@ export function withApollo( ) { const withDisplayName = `withApollo(${getDisplayName(WrappedComponent)})`; - class WithApollo extends Component { + class WithApollo extends React.Component { static displayName = withDisplayName; static WrappedComponent = WrappedComponent; - static contextTypes = { client: PropTypes.object.isRequired }; - - // data storage - private client: ApolloClient; // apollo client // wrapped instance private wrappedInstance: any; - constructor(props, context) { - super(props, context); - this.client = context.client; + constructor(props) { + super(props); this.setWrappedInstance = this.setWrappedInstance.bind(this); - - invariant( - !!this.client, - `Could not find "client" in the context of ` + - `"${withDisplayName}". ` + - `Wrap the root component in an `, - ); } getWrappedInstance() { @@ -59,10 +44,19 @@ export function withApollo( } render() { - const props = assign({}, this.props); - props.client = this.client; - if (operationOptions.withRef) props.ref = this.setWrappedInstance; - return createElement(WrappedComponent, props); + return ( + + {client => ( + + )} + + ); } } diff --git a/test/flow-usage.js b/test/flow-usage.js index 771491d312..8320f6fb1a 100644 --- a/test/flow-usage.js +++ b/test/flow-usage.js @@ -143,22 +143,23 @@ export type InputProps = { episode: string, }; -const withCharacter: OperationComponent = graphql( - HERO_QUERY, - { - options: ({ episode }) => ({ - // $ExpectError [string] This type cannot be compared to number - variables: { episode: episode > 1 }, - }), - props: ({ data, ownProps }) => ({ - ...data, - // $ExpectError [string] This type cannot be compared to number - episode: ownProps.episode > 1, - // $ExpectError property `isHero`. Property not found on object type - isHero: data && data.hero && data.hero.isHero, - }), - }, -); +const withCharacter: OperationComponent< + Response, + InputProps, + Props, +> = graphql(HERO_QUERY, { + options: ({ episode }) => ({ + // $ExpectError [string] This type cannot be compared to number + variables: { episode: episode > 1 }, + }), + props: ({ data, ownProps }) => ({ + ...data, + // $ExpectError [string] This type cannot be compared to number + episode: ownProps.episode > 1, + // $ExpectError property `isHero`. Property not found on object type + isHero: data && data.hero && data.hero.isHero, + }), +}); export default withCharacter(({ loading, hero, error }) => { if (loading) return
Loading
; diff --git a/test/react-native/component.test.tsx b/test/react-native/component.test.tsx index 65e8ab35da..41812539cf 100644 --- a/test/react-native/component.test.tsx +++ b/test/react-native/component.test.tsx @@ -33,12 +33,12 @@ describe('App', () => { cache: new Cache({ addTypename: false }), }); - const ContainerWithData = graphql(query)( - ({ data }: ChildProps<{}, Data>) => { - if (data.loading) return Loading...; - return {data.allPeople.people.name}; - }, - ); + const ContainerWithData = graphql( + query, + )(({ data }: ChildProps<{}, Data>) => { + if (data.loading) return Loading...; + return {data.allPeople.people.name}; + }); const output = renderer.create( diff --git a/test/react-web/client/ApolloConsumer.test.js b/test/react-web/client/ApolloConsumer.test.js new file mode 100644 index 0000000000..e742cb12d0 --- /dev/null +++ b/test/react-web/client/ApolloConsumer.test.js @@ -0,0 +1,57 @@ +import React from 'react'; +import ApolloClient from 'apollo-client'; +import { InMemoryCache as Cache } from 'apollo-cache-inmemory'; +import { ApolloLink } from 'apollo-link'; + +import ApolloProvider from '../../../src/ApolloProvider'; +import ApolloConsumer from '../../../src/ApolloConsumer'; +import { mount } from 'enzyme'; +import invariant from 'invariant'; + +const client = new ApolloClient({ + cache: new Cache(), + link: new ApolloLink((o, f) => f(o)), +}); + +describe(' component', () => { + it('has a render prop', done => { + mount( + + + {clientRender => { + try { + expect(clientRender).toBe(client); + done(); + } catch (e) { + done.fail(e); + } + return null; + }} + + , + ); + }); + + it('renders the content in the render prop', () => { + const wrapper = mount( + + {clientRender =>
} + , + ); + + expect(wrapper.find('div').exists()).toBeTruthy(); + }); + + it('errors if there is no client in the context', () => { + // Prevent Error about missing context type from appearing in the console. + const errorLogger = console.error; + console.error = () => {}; + expect(() => { + mount( null} />); + }).toThrowError( + 'Could not find "client" in the context of ApolloConsumer. Wrap the root component in an ', + ); + + console.error = errorLogger; + }); +}); diff --git a/test/react-web/client/graphql/queries/index.test.tsx b/test/react-web/client/graphql/queries/index.test.tsx index 0821ad9eb3..2198685136 100644 --- a/test/react-web/client/graphql/queries/index.test.tsx +++ b/test/react-web/client/graphql/queries/index.test.tsx @@ -44,13 +44,13 @@ describe('queries', () => { }; } - const ContainerWithData = graphql(query)( - ({ data }: DataProps) => { - expect(data).toBeTruthy(); - expect(data.loading).toBeTruthy(); - return null; - }, - ); + const ContainerWithData = graphql( + query, + )(({ data }: DataProps) => { + expect(data).toBeTruthy(); + expect(data.loading).toBeTruthy(); + return null; + }); const output = renderer.create( @@ -95,13 +95,13 @@ describe('queries', () => { first: number; } - const ContainerWithData = graphql(query)( - ({ data }: DataProps) => { - expect(data).toBeTruthy(); - expect(data.variables).toEqual(variables); - return null; - }, - ); + const ContainerWithData = graphql( + query, + )(({ data }: DataProps) => { + expect(data).toBeTruthy(); + expect(data.variables).toEqual(variables); + return null; + }); renderer.create( diff --git a/test/react-web/client/graphql/subscriptions.test.tsx b/test/react-web/client/graphql/subscriptions.test.tsx index a607ef58d5..babb5c4eda 100644 --- a/test/react-web/client/graphql/subscriptions.test.tsx +++ b/test/react-web/client/graphql/subscriptions.test.tsx @@ -47,14 +47,14 @@ describe('subscriptions', () => { user: { name: string }; } - const ContainerWithData = graphql(query)( - ({ data }: ChildProps) => { - expect(data).toBeTruthy(); - expect(data.user).toBeFalsy(); - expect(data.loading).toBeTruthy(); - return null; - }, - ); + const ContainerWithData = graphql( + query, + )(({ data }: ChildProps) => { + expect(data).toBeTruthy(); + expect(data.user).toBeFalsy(); + expect(data.loading).toBeTruthy(); + return null; + }); const output = renderer.create( @@ -84,13 +84,13 @@ describe('subscriptions', () => { user: { name: string }; } - const ContainerWithData = graphql(query)( - ({ data }: ChildProps) => { - expect(data).toBeTruthy(); - expect(data.variables).toEqual(variables); - return null; - }, - ); + const ContainerWithData = graphql( + query, + )(({ data }: ChildProps) => { + expect(data).toBeTruthy(); + expect(data.variables).toEqual(variables); + return null; + }); const output = renderer.create( diff --git a/test/react-web/server/getDataFromTree.test.tsx b/test/react-web/server/getDataFromTree.test.tsx index 8acdaa424d..9fda2fa5f3 100644 --- a/test/react-web/server/getDataFromTree.test.tsx +++ b/test/react-web/server/getDataFromTree.test.tsx @@ -254,11 +254,11 @@ describe('SSR', () => { firstName: string; }; } - const WrappedElement = graphql(query)( - ({ data }: ChildProps) => ( -
{data.loading ? 'loading' : data.currentUser.firstName}
- ), - ); + const WrappedElement = graphql( + query, + )(({ data }: ChildProps) => ( +
{data.loading ? 'loading' : data.currentUser.firstName}
+ )); const app = ( @@ -381,11 +381,11 @@ describe('SSR', () => { }; } - const WrappedElement = graphql(query)( - ({ data }: ChildProps) => ( -
{data.loading ? 'loading' : data.currentUser.firstName}
- ), - ); + const WrappedElement = graphql( + query, + )(({ data }: ChildProps) => ( +
{data.loading ? 'loading' : data.currentUser.firstName}
+ )); const Page = () => (
@@ -494,11 +494,11 @@ describe('SSR', () => { firstName: string; }; } - const WrappedElement = graphql(query)( - ({ data }: ChildProps) => ( -
{data.loading ? 'loading' : data.error}
- ), - ); + const WrappedElement = graphql( + query, + )(({ data }: ChildProps) => ( +
{data.loading ? 'loading' : data.error}
+ )); const Page = () => (
@@ -551,11 +551,11 @@ describe('SSR', () => { firstName: string; }; } - const WrappedElement = graphql(query, { skip: true })( - ({ data }: ChildProps) => ( -
{!data ? 'skipped' : 'dang'}
- ), - ); + const WrappedElement = graphql(query, { + skip: true, + })(({ data }: ChildProps) => ( +
{!data ? 'skipped' : 'dang'}
+ )); const app = ( @@ -596,11 +596,11 @@ describe('SSR', () => { firstName: string; }; } - const Element = graphql(query, { name: 'user' })( - ({ user }: ChildProps & { user: DataValue }) => ( -
{user.loading ? 'loading' : user.currentUser.firstName}
- ), - ); + const Element = graphql(query, { + name: 'user', + })(({ user }: ChildProps & { user: DataValue }) => ( +
{user.loading ? 'loading' : user.currentUser.firstName}
+ )); const app = ( @@ -1013,11 +1013,11 @@ describe('SSR', () => { cache: new Cache({ addTypename: false }), }); - const WrappedElement = graphql(query)( - ({ data }: ChildProps<{}, Data>) => ( -
{data.loading ? 'loading' : data.currentUser.firstName}
- ), - ); + const WrappedElement = graphql( + query, + )(({ data }: ChildProps<{}, Data>) => ( +
{data.loading ? 'loading' : data.currentUser.firstName}
+ )); class MyRootContainer extends React.Component { constructor(props) {