Is there a way to detect when data used by a query is evicted from the Apollo Client cache?
Asked Answered
W

1

17

Apologies for the long question, but I'd be really grateful for some thoughts/help on the best strategy for cache invalidation & refetching queries in Apollo Client 3.

Background

First, some information about the scenario I'm imagining:

  • There is an Account component (example below) which uses the useQuery hook from react-apollo to fetch & display some basic information about an account and a list of transactions for that account
  • Elsewhere in the app, there is a CreateTransactionForm component that uses a mutation to insert a new transaction. This is a separate component which lives at a different location in the component tree and is not necessarily a child of the AccountComponent)
  • Crucially, the process of storing a transaction on the server has some non-trivial side effects besides inserting the actual transaction into the database:
    • all other transactions which occur after the one being inserted (chronologically) get updated with new running balances
    • any related account(s) are updated with a new current balance

A simplistic version of my Account component might look something like this:

import { gql, useQuery } from '@apollo/client';
import React from 'react';
import { useParams } from 'react-router-dom';

const GET_ACCOUNT_WITH_TRANSACTIONS = gql`
  query getAccountWithTransactions($accountId: ID!) {
    account(accountId: $accountId) {
      _id
      name
      description
      currentBalance
      transactions {
        _id
        date
        amount
        runningBalance
      }
    }
  }
`;

export const Account: React.FunctionComponent = () => {
  const { accountId } = useParams();
  const { loading, error, data } = useQuery(GET_ACCOUNT_WITH_TRANSACTIONS, {
    variables: { accountId },
  });

  if (loading) { return <p>Loading...</p>; }
  if (error) { return <p>Error</p>; }

  return (
    <div>
      <h1>{data.account.name}</h1>

      {data.account.transactions.map(transaction => (
        <TransactionRow key={transaction._id} transaction={transaction} />
      ))}
    </div>
  );
};

Potential strategies

I'm evaluating the various options for invalidating parts of the Apollo Client cache and refetching the appropriate data after inserting a transaction. From what I've learned so far, there are a few potential strategies:

a) call the refetch method returned by useQuery to force the Account component to reload its data

  • this seems reliable and would go back to the server for fresh data, but the CreateTransactionForm would need to be (directly or indirectly) coupled to the Account component because something needs to trigger that call to refetch

b) pass the query name (getAccountWithTransactions) into the refetchQueries option of the mutation

  • similar to a, but with potentially even tighter coupling - the CreateTransactionForm would need to have knowledge about every other component/query that exists in the app and could be affected by the mutation (and if more are added in the future, it would mean remembering to update the CreateTransactionForm)

c) manually modify the contents of the cache after performing mutations

  • I imagine this would be quite complex/hard to maintain because the CreateTransactionForm would need to know exactly what data has changed as a result of the server's actions. As mentioned, this might not be a trivial amount of data and after performing the mutation we'd need to retrieve updated data not only about the transaction that was inserted but also any others that have been updated as a side effect, plus affected accounts, etc. It also might not be very efficient because some of that information may never be viewed in the client again.

My intuition is that none of the options above feel ideal. In particular, I am worried about maintainability as the app grows; if components need to have explicit knowledge about exactly which other components/queries may be affected by changes made to the data graph, then it feels like it would be very easy to miss one and introduce subtle bugs once the app grows to be larger and more complex.

A better way?

I am very interested in the new evict and gc methods introduced in Apollo Client 3 and am wondering whether they could provide a neater solution.

What I'm considering is, after calling the mutation I could use these new capabilities to:

  • aggressively evict the transactions array on all accounts that are included in the transaction
  • also, evict the currentBalance field on any affected accounts

for example:

  const { cache } = useApolloClient();
  ...
  // after calling the mutation:
  cache.evict(`Account:${accountId}`, 'transactions');
  cache.evict(`Account:${accountId}`, 'currentBalance');
  cache.gc();

The above provides an easy way to remove stale data from the cache and ensures that the components will go to the network the next time those fields queries are performed. This works well if I navigate away to a different page and back to the Account page, for example.

My main question (finally!)

This leads onto the main piece of the puzzle that I'm unsure about:

is there any way to detect that some or all of the data referenced in a query has been evicted from the cache?

I'm not sure if this is a feasible thing to expect of the library, but if it's possible I think it could result in simpler code and less coupling between different parts of the app.

My thinking is that this would allow each component to become more "reactive" - the components simply know which data they depend on and whenever that data goes missing from the underlying cached graph it could immediately react by triggering a refetch on its own query. It would be nice for components to be declaratively reacting to changes in the data that they depend on, rather than imperatively communicating to triggering actions on each other if that makes sense.

Westernize answered 21/2, 2020 at 18:56 Comment(2)
I'm having success at triggering refetches by evicting fields. I just wish it was better documented.Kwangtung
It seems the intended behavior (and actual behavior for me currently) is for cache eviction to invalidate cached queries that referenced the evicted member (so they rerun next time instead of using the cache). Closest thing to documentation I could find was this comment by one of the project maintainers.Farce
F
1

There are two answers here, what is the standard practice way to do this? And how can we possibly make this Better Way?

Standard Practice

https://www.apollographql.com/blog/when-to-use-refetch-queries-in-apollo-client/

Apollo on their blog and on their documentation state that if you mutation has side effects outside of the returned query then you must use an update function if you want to maintain cache consistency. This is similar to using refetchQueries but is more explicit as you are doing the updating of the cache yourself (it also takes less network requests). Although, you are right that this approach will lead to coupling between your transaction's mutation update and all other dependent components.

I believe the best way to structure it would then be to define an update function for all components whose mutation has side effects. And then transaction would call all components update functions that it effects. And these in turn would call their own dependencies. This could get messy so let us see if you could do the cache control yourself.

The Better Way

https://www.apollographql.com/docs/apollo-server/data/data-sources/#using-memcachedredis-as-a-cache-storage-backend

I don't believe you can check the Apollo cache for if data is present, but you could check a Redis cache for this same information and Apollo allows you to plug in other cache implementations. You could even write your own cache implementation and use Redis as a backend to your implementation. This would allow you the finer control over the cache that you want. But, this comes at a massive upfront engineering cost.

Conclusion:

Since your side effects seem to be within the transaction and account components (only two components), I would say you should write a custom update function for now. Once you have a more complex cache coherency problem I would think about diving into a custom caching implementation.

Fugazy answered 29/3, 2021 at 11:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.