How to refetch a query when a new subscription arr

2019-05-20 04:32发布

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>

2条回答
▲ chillily
2楼-- · 2019-05-20 05:00

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.

查看更多
孤傲高冷的网名
3楼-- · 2019-05-20 05:15

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);
查看更多
登录 后发表回答