From c0a6b22e0c3a65bc829276829f7159632183499f Mon Sep 17 00:00:00 2001 From: Roman Coedo Date: Tue, 8 Aug 2017 14:38:50 +0200 Subject: [PATCH] Client scoped query recyclers (#513) --- Changelog.md | 1 + src/ApolloProvider.tsx | 7 ++- src/QueryRecyclerProvider.tsx | 50 +++++++++++++++++++ src/graphql.tsx | 20 ++++---- .../client/graphql/client-option.test.tsx | 3 +- 5 files changed, 69 insertions(+), 12 deletions(-) create mode 100644 src/QueryRecyclerProvider.tsx diff --git a/Changelog.md b/Changelog.md index 4a7d6fc682..2bcf252f99 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,6 +1,7 @@ # Change log ### vNext +- Fix: Scope query recyclers by client [#876](https://github.com/apollographql/react-apollo/pull/876) - Replace string refs with callback refs [#908](https://github.com/apollographql/react-apollo/pull/908) ### 1.4.10 diff --git a/src/ApolloProvider.tsx b/src/ApolloProvider.tsx index 4d7cdf98ed..e35c8ed140 100644 --- a/src/ApolloProvider.tsx +++ b/src/ApolloProvider.tsx @@ -5,6 +5,7 @@ import { Component } from 'react'; import { Store } from 'redux'; import ApolloClient, { ApolloStore } from 'apollo-client'; +import QueryRecyclerProvider from './QueryRecyclerProvider'; const invariant = require('invariant'); @@ -61,6 +62,10 @@ export default class ApolloProvider extends Component { } render() { - return React.Children.only(this.props.children); + return ( + + {React.Children.only(this.props.children)} + + ); } } diff --git a/src/QueryRecyclerProvider.tsx b/src/QueryRecyclerProvider.tsx new file mode 100644 index 0000000000..7ae6fe3f4c --- /dev/null +++ b/src/QueryRecyclerProvider.tsx @@ -0,0 +1,50 @@ +import { Component, Children } from 'react'; +import * as PropTypes from 'prop-types'; +import { ObservableQueryRecycler } from './queryRecycler'; + +class QueryRecyclerProvider extends Component { + static propTypes = { + children: PropTypes.element.isRequired + }; + + static contextTypes = { + client: PropTypes.object + }; + + static childContextTypes = { + getQueryRecycler: PropTypes.func.isRequired + }; + + private recyclers: WeakMap; + + constructor(props) { + super(props); + this.recyclers = new WeakMap(); + this.getQueryRecycler = this.getQueryRecycler.bind(this); + } + + componentWillReceiveProps(nextProps, nextContext) { + if (this.context.client !== nextContext.client) { + this.recyclers = new WeakMap(); + } + } + + getQueryRecycler(component) { + if (!this.recyclers.has(component)) { + this.recyclers.set(component, new ObservableQueryRecycler()); + } + return this.recyclers.get(component); + } + + getChildContext() { + return ({ + getQueryRecycler: this.getQueryRecycler + }); + } + + render() { + return Children.only(this.props.children); + } +} + +export default QueryRecyclerProvider; diff --git a/src/graphql.tsx b/src/graphql.tsx index 657a3e5a34..0c4791411d 100644 --- a/src/graphql.tsx +++ b/src/graphql.tsx @@ -98,19 +98,12 @@ export default function graphql< function wrapWithApolloComponent(WrappedComponent) { const graphQLDisplayName = `${alias}(${getDisplayName(WrappedComponent)})`; - // A recycler that we can use to recycle old observable queries to keep - // them hot between component unmounts and remounts. - // - // Note that the existence of this recycler could potentially cause memory - // leaks if many components are being created and unmounted in parallel. - // However, this is an unlikely scenario. - const recycler = new ObservableQueryRecycler(); - class GraphQL extends Component { static displayName = graphQLDisplayName; static WrappedComponent = WrappedComponent; static contextTypes = { client: PropTypes.object, + getQueryRecycler: PropTypes.func, }; // react / redux and react dev tools (HMR) needs @@ -121,6 +114,7 @@ export default function graphql< // data storage private store: ApolloStore; private client: ApolloClient; // apollo client + private recycler: ObservableQueryRecycler; private type: DocumentType; // request / action storage. Note that we delete querySubscription if we @@ -142,6 +136,7 @@ export default function graphql< constructor(props, context) { super(props, context); + this.version = version; this.type = operation.type; this.dataForChildViaMutation = this.dataForChildViaMutation.bind(this); @@ -183,6 +178,7 @@ export default function graphql< this.unsubscribeFromQuery(); this.queryObservable = null; this.previousData = {}; + this.updateQuery(nextProps); if (!this.shouldSkip(nextProps)) { this.subscribeToQuery(); @@ -219,7 +215,7 @@ export default function graphql< if (this.type === DocumentType.Query) { // Recycle the query observable if there ever was one. if (this.queryObservable) { - recycler.recycle(this.queryObservable); + this.getQueryRecycler().recycle(this.queryObservable); delete this.queryObservable; } @@ -237,6 +233,10 @@ export default function graphql< this.hasMounted = false; } + getQueryRecycler() { + return this.context.getQueryRecycler(GraphQL); + } + getClient(): ApolloClient { if (this.client) return this.client; const { client } = mapPropsToOptions(this.props); @@ -337,7 +337,7 @@ export default function graphql< // Try to reuse an `ObservableQuery` instance from our recycler. If // we get null then there is no instance to reuse and we should // create a new `ObservableQuery`. Otherwise we will use our old one. - const queryObservable = recycler.reuse(opts); + const queryObservable = this.getQueryRecycler().reuse(opts); if (queryObservable === null) { this.queryObservable = this.getClient().watchQuery( diff --git a/test/react-web/client/graphql/client-option.test.tsx b/test/react-web/client/graphql/client-option.test.tsx index f7f686a771..52ed6dc22e 100644 --- a/test/react-web/client/graphql/client-option.test.tsx +++ b/test/react-web/client/graphql/client-option.test.tsx @@ -9,6 +9,7 @@ import gql from 'graphql-tag'; import ApolloClient, { ApolloError, ObservableQuery } from 'apollo-client'; import { mockNetworkInterface } from '../../../../src/test-utils'; import { ApolloProvider, graphql } from '../../../../src'; +import { ObservableQueryRecycler } from '../../../../src/queryRecycler'; describe('client option', () => { it('renders with client from options', () => { @@ -33,7 +34,7 @@ describe('client option', () => { }, }; const ContainerWithData = graphql(query, config)(props => null); - shallow(); + shallow(, { context: { getQueryRecycler: () => new ObservableQueryRecycler() }}); }); it('ignores client from context if client from options is present', done => {