I was wondering if there's an elegant way to trigger the refetch of a query in react-apollo when a subscription receives new data (The data is not important here and will be the same as previous one). I just use subscription here as a notification trigger that tells Query to refetch.
I tried both using Subscription component and subscribeToMore to call "refetch" method in Query's child component but both methods cause infinite re-fetches.
NOTE: I'm using react-apollo v2.1.3 and apollo-client v2.3.5
here's the simplified version of code
<Query
query={GET_QUERY}
variables={{ blah: 'test' }}
>
{({ data, refetch }) => (
<CustomComponent data={data} />
//put subscription here? It'll cause infinite re-rendering/refetch loop
)}
<Query>
It's possible if you use componentDidMount
and componentDidUpdate
in the component rendered by the Subscription render props function.
The example uses recompose
higher order components to avoid too much boilerplating. Would look something like:
/*
* Component rendered when there's data from subscription
*/
export const SubscriptionHandler = compose(
// This would be the query you want to refetch
graphql(QUERY_GQL, {
name: 'queryName'
}),
lifecycle({
refetchQuery() {
// condition to refetch based on subscription data received
if (this.props.data) {
this.props.queryName.refetch()
}
},
componentDidMount() {
this.refetchQuery();
},
componentDidUpdate() {
this.refetchQuery();
}
})
)(UIComponent);
/*
* Component that creates the subscription operation
*/
const Subscriber = ({ username }) => {
return (
<Subscription
subscription={SUBSCRIPTION_GQL}
variables={{ ...variables }}
>
{({ data, loading, error }) => {
if (loading || error) {
return null;
}
return <SubscriptionHandler data={data} />;
}}
</Subscription>
);
});
Another way of accomplishing this while totally separating Query and Subscription components, avoiding loops on re-rendering is using Apollo Automatic Cache updates:
+------------------------------------------+
| |
+----------->| Apollo Store |
| | |
| +------------------------------+-----------+
+ |
client.query |
^ +-----------------+ +---------v-----------+
| | | | |
| | Subscription | | Query |
| | | | |
| | | | +-----------------+ |
| | renderNothing | | | | |
+------------+ | | | Component | |
| | | | | |
| | | +-----------------+ |
| | | |
+-----------------+ +---------------------+
const Component =() => (
<div>
<Subscriber />
<QueryComponent />
</div>
)
/*
* Component that only renders Query data
* updated automatically on query cache updates thanks to
* apollo automatic cache updates
*/
const QueryComponent = graphql(QUERY_GQL, {
name: 'queryName'
})(() => {
return (
<JSX />
);
});
/*
* Component that creates the subscription operation
*/
const Subscriber = ({ username }) => {
return (
<Subscription
subscription={SUBSCRIPTION_GQL}
variables={{ ...variables }}
>
{({ data, loading, error }) => {
if (loading || error) {
return null;
}
return <SubscriptionHandler data={data} />;
}}
</Subscription>
);
});
/*
* Component rendered when there's data from subscription
*/
const SubscriptionHandler = compose(
// This would be the query you want to refetch
lifecycle({
refetchQuery() {
// condition to refetch based on subscription data received
if (this.props.data) {
var variables = {
...this.props.data // if you need subscription data for the variables
};
// Fetch the query, will automatically update the cache
// and cause QueryComponent re-render
this.client.query(QUERY_GQL, {
variables: {
...variables
}
});
}
},
componentDidMount() {
this.refetchQuery();
},
componentDidUpdate() {
this.refetchQuery();
}
}),
renderNothing
)();
/*
* Component that creates the subscription operation
*/
const Subscriber = ({ username }) => {
return (
<Subscription
subscription={SUBSCRIPTION_GQL}
variables={{ ...variables }}
>
{({ data, loading, error }) => {
if (loading || error) {
return null;
}
return <SubscriptionHandler data={data} />;
}}
</Subscription>
);
});
Note:
compose
and lifecycle
are recompose methods that enable easier a cleaner higher order composition.
Finally I figured it out myself with the inspiration from Pedro's answer.
Thoughts: the problem I'm facing is that I want to call Query's refetch method in Subscription, however, both Query and Subscription components can only be accessed in render method. That is the root cause of infinite refetch/re-rendering. To solve the problem, we need to move the subscription logic out of render method and put it somewhere in a lifecycle method (i.e. componentDidMount) where it won't be called again after a refetch is triggered. Then I decided to use graphql hoc instead of Query component so that I can inject props like refetch, subscribeToMore at the top level of my component, which makes them accessible from any life cycle methods.
Code sample (simplified version):
class CustomComponent extends React.Component {
componentDidMount() {
const { data: { refetch, subscribeToMore }} = this.props;
this.unsubscribe = subscribeToMore({
document: <SUBSCRIBE_GRAPHQL>,
variables: { test: 'blah' },
updateQuery: (prev) => {
refetch();
return prev;
},
});
}
componentWillUnmount() {
this.unsubscribe();
}
render() {
const { data: queryResults, loading, error } } = this.props;
if (loading || error) return null;
return <WhatEverYouWant with={queryResults} />
}
}
export default graphql(GET_QUERY)(CustomComponent);