javascriptreactjscachinggraphqlurql

How do I update the grapqhl cache with urql upon a mutation, where the initial query response does not include the required __typename?


My situation has 4 components nested within each other in this order: Products (page), ProductList, ProductListItem, and CrossSellForm.

Products executes a graphql query (using urql) as such:

const productsQuery = `
  query {
    products {
      id
      title
      imageSrc
      crossSells {
        id
        type
        title
      }
    }
  }
`;

...

const [response] = useQuery({
    query: productsQuery,
  });
  const { data: { products = [] } = {}, fetching, error } = response;

...

 <ProductList products={products} />

products returns an array of Products that contains a field, crossSells, that returns an array of CrossSells. Products is propagated downwards to CrossSellForm, which contains a mutation query that returns an array of CrossSells.

The problem is that when I submit the crossSellForm the request goes through successfully but the crossSells up in Products does not update, and the UI reflects stale data. This only happens when the initial fetch up in Products contains no crossSells, so the initial response looks something like this:

{
data: {
      products: [
        {
          id: '123123',
          title: 'Nice',
          imageSrc: 'https://image.com',
          crossSells: [],
          __typename: "Product"
        },
        ...
      ]
    }
}
}

If there is an existing crossSell, there is no problem, the ui updates properly and the response looks like this:

{
  data: {
      products: [
        {
          id: '123123',
          title: 'Nice',
          imageSrc: 'https://image.com',
          crossSells: [
            {
              id: 40,
              title: 'Nice Stuff',
              type: 'byVendor',
              __typename: 'CrossSell'
            }
          ],
          __typename: "Product"
        },
        ...
      ]
    }
  }
}

I read up a bit on urql's caching mechanism at https://formidable.com/open-source/urql/docs/basics/ and from what I understand it uses a document cache, so it caches the document based on __typename. If a query requests something with a the same __typename it will pull it from the cache. If a mutation occurs with the same __typename it will invalidate all objects in the cache with that __typename so the next time the user fetches an object with that __typename it will execute a network request instead of cache.

What I think is going on is in the initial situation where there are products but no crossSells the form submission is successful but the Products page does not update because there is no reference to an object with __typename of CrossSell, but in the second situation there is so it busts the cache and executes the query again, refreshes products and cross-sells and the UI is properly updated.

I've really enjoyed the experience of using urql hooks with React components and want to continue but I'm not sure how I can fix this problem without reaching for another tool.

I've tried to force a re-render upon form submission using tips from: How can I force component to re-render with hooks in React? but it runs into the same problem where Products will fetch from the cache again and crossSells will return an empty array. I thought about modifying urql's RequestPolicy to network only, along with the forced re-render, but I thought that would be unnecessarily expensive to re-fetch every single time. The solution I'm trying out now is to move all the state into redux, a single source of truth so that any update to crossSells will propagate properly, and although I'm sure it will work it will also mean I'll trade in a lot of the convenience I had with hooks for standard redux boilerplate.

How can I gracefully update Products with crossSells upon submitting the form within CrossSellForm, while still using urql and hooks?


Solution

  • core contributor here 👋

    As you've already discovered, there's an open issue for this that details the inherent problem our our simple, default cache. It's a document cache so kind of unsuitable for more complex tasks where normalisation can help.

    When we have am empty array of data, there's no indication that a specific result needs to be refetched.

    Instead of using the network-only policy you could try cache-and-network, but that doesn't solve the underlying issue that the operation (your query) is not invalidated by the mutation. So no refetch will be triggered.

    I'd very much recommend you Graphcache, our normalised cache, which you've also already discovered. At its minimum with no configuration (!) it's actually a drop-in replacement that's already quite a bit smarter. https://github.com/FormidableLabs/urql-exchange-graphcache

    The configuration for it is really just addons to teach it how to handle more tasks automatically! I'd be happy to help you in issues, here, or via Spectrum if you need to customise it. But my advise would be, give it a shot, because in the best case, you'll have all your edge cases just working without any changes ✨