How to force Apollo Query component to re-run query when parent component re-renders
Asked Answered
D

5

21

I'm using Apollo Client's <Query> within a component that is re-rendered when state is changed within a lifecycle method. I wish to have my <Query> component re-run the query because I know that data has changed. It appears that Query component is using a cache that needs to be invalidated before query is re-run.

I'm using a wonky workaround that caches the refetch callback from the render prop in the parent component, but it feels wrong. I'll post my approach in the answers if anyone is interested.

My code looks something like this. I removed loading and error handling from query as well as some other detail for brevity.

class ParentComponent extends React.Component {
  componentDidUpdate(prevProps) {
    if (this.props.refetchId !== prevProps.refetchId) {
      const otherData = this.processData() // do something
      this.setState({otherData}) // this forces component to reload
    }
  }

  render() {
    const { otherData } = this.state

    return (
      <Query query={MY_QUERY}>
        {({ data }) => {
          return <ChildComponent gqlData={data} stateData={otherData} />
        }}
      </Query>
    )
  }
}

How do I force <Query> to fetch new data to pass to <ChildComponent>?

Even though ParentComponent re-renders when props or state change, Query doesn't re-run. ChildComponent gets an updated stateData prop, but has a stale gqlData prop. As I understand Apollo's query cache need to be invalidated, but I'm not sure.

Please note that passing refetch to ChildComponent is not the answer because it only displays information from GraphQL and wouldn't know when to refetch. I don't want to introduce timers or otherwise complicate ChildComponent to solve this - it doesn't need to know about this complexity or data fetching concerns.

Dodgem answered 26/7, 2018 at 3:18 Comment(1)
I added couple options, I always used the first one, the second method would not work if you do not want to pass anything down.Angeli
T
21

I had almost a similar situation which I solved with fetchPolicy attribute:

<Query fetchPolicy="no-cache" ...

The task was to load some details from server by clicking on a list item.

And if I wanted to add an action to force re-fetching the query (such as modifying the details), I first assigned the refetch to this.refetch:

<Query fetchPolicy="no-cache" query={GET_ACCOUNT_DETAILS} variables=... }}>
  {({ data: { account }, loading, refetch }) => {
  this.refetch = refetch;
...

And in the specific action that I wanted the refetch to happen, I called:

this.refetch();
Trim answered 9/4, 2019 at 14:27 Comment(2)
This was the simplest solution for meAxolotl
This worked like a charm. Easiest solution I've found so far.Reservation
B
3

In addition to the already accepted answer above there are 2 ways of achieving this.

If all you need is getting data from the graphql server to generate a list every time a component is mounted you have these options:

  1. Use the option fetchPolicy="no-cache"
export default defineComponent({
  setup() {
    const { result, loading, error } = useMyQuery(
      () => {
        return { date: new Date() }
      },
      {
        pollInterval: 15 * 60 * 1000, // refresh data every 15 min
        fetchPolicy: 'no-cache',
      }
    )
  1. Use the refetch() method while keeping the cache in place:
export default defineComponent({
  setup() {
    const { result, loading, error}, refetch } = useMyQuery(
      () => {
        return { date: new Date() }
      },
      {
        pollInterval: 15 * 60 * 1000, // refresh data every 15 min
      }
    )

    if (!loading.value) void refetch()
Betulaceous answered 1/12, 2020 at 12:55 Comment(0)
A
2

Could you refetch in parent component? Once the parent component get an update, then you can evaluate whether to trigger a fetch or not.

I have done it without using Query like the following:

class ParentComp extends React.Component {

    lifeCycleHook(e) { //here
        // call your query here
        this.props.someQuery()
    }

    render() {
        return (
            <div>
                <Child Comp data={this.props.data.key}> //child would only need to render data
            </div>
        );
    }
}

export default graphql(someQuery)(SongCreate);

So you can trigger your fetch anytime you want it to. You can get the query as a prop in this case.

For your case, you would put your query into a prop using export default graphql(addSongQuery)(SongCreate);. Then call it in your lifecyclehooks DidUpdate.

Another options is to use refetch on Query.

<Query
    query={GET_DOG_PHOTO}
    variables={{ breed }}
    skip={!breed}
  >
    {({ loading, error, data, refetch }) => {
      if (loading) return null;
      if (error) return `Error!: ${error}`;

      return (
        <div>
          <img
            src={data.dog.displayImage}
            style={{ height: 100, width: 100 }}
          />
          <button onClick={() => refetch()}>Refetch!</button>
        </div>
      );
    }}
  </Query>

The second method would require you pass something down to your child, which isn't really all that bad.

Angeli answered 26/7, 2018 at 3:34 Comment(4)
in both of your examples child component does something to trigger a rerender. as described in the question, child component doesn't know when to trigger a rerender (it's not interactive). thanks, but this is not really what i'm afterDodgem
no, the first one does not. The parent has the query as a prop, and you can re-render using lifecycle hooks.Angeli
ah, I see. now that you've simplified it, it's easier to follow. thanks.Dodgem
Great! Glad to be working @AndreiR. Let me know how it turns out. If works, could you up-vote and accept the answer for other viewers? ThanksAngeli
H
2

It seems to me that the Query component doesn't necessarily need to be inside this ParentComponent.

In that case, I would move the Query component up, since I would still be able to render other stuff while I don't have results in the ChildComponent. And then I would have access to the query.refetch method.

Note that in the example I added the graphql hoc, but you can still use Query component around <ParentComponent />.

class ParentComponent extends React.Component {
    componentDidUpdate(prevProps) {
        if (this.props.refetchId !== prevProps.refetchId) {
            const otherData = this.processData() // do something

            //   won't need this anymore, since refetch will cause the Parent component to rerender)
            //   this.setState({otherData}) // this forces component to reload 

            this.props.myQuery.refetch(); // >>> Refetch here!
        }
    }

    render() {
        const {
            otherData
        } = this.state;

        return <ChildComponent gqlData={this.props.myQuery} stateData={otherData} />;
    }
}

export graphql(MY_QUERY, {
    name: 'myQuery'
})(ParentComponent);
Handsomely answered 26/7, 2018 at 12:9 Comment(2)
Thanks. This opened my eyes a bit. Basically, <Query> is a poor choice for almost any use case other than the most trivial ones.Dodgem
Yes. I end up not using it that much. This this solve your problem?Handsomely
D
0

As promised, here's my hacky workaround

class ParentComponent extends React.Component {
  componentDidUpdate(prevProps) {
    if (this.props.refetchId !== prevProps.refetchId) {
      const otherData = this.processData() // do something
      this.setState({otherData})
      if (this.refetch) {
        this.refetch()
      }
    }
  }

  render() {
    const { otherData } = this.state
    const setRefetch = refetch => {this.refetch = refetch}
    return (
      <Query query={MY_QUERY}>
        {({ data, refetch }) => {
          setRefetch(refetch)
          return <ChildComponent gqlData={data} stateData={otherData} />
        }}
      </Query>
    )
  }
}
Dodgem answered 4/3, 2019 at 2:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.