Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Apollo cache: reuse same derived data between different fields #8052

Open
jdmoliner opened this issue Apr 26, 2021 · 7 comments
Open

Apollo cache: reuse same derived data between different fields #8052

jdmoliner opened this issue Apr 26, 2021 · 7 comments

Comments

@jdmoliner
Copy link

jdmoliner commented Apr 26, 2021

I have an server schema like this:

User {
 id
 amount
}

Match {
 id
 date
 probability
}

Then I need to compute derived data before render. Then I want to do something like this:

const typePolicies = {
 Match {
  fields: {
   odd: {
    read (_ , { readField })  {
      
      const amountUser = cache.readFragment({
        id: 'User:{}',
        fragment: gql`
          fragment amountUser on User {
           amount                          
          }`
      });

      const probabilityMatch = readField('probability');    
      
      const calc = getCalc (amountUser, probabilityMatch); // complex operation
      const odd = getOdd (calc);
      return odd;
    }
   },
   maxValue: {
    read (_ , { readField })  {
      
      const amountUser = cache.readFragment({
        id: 'User:{}',
        fragment: gql`
          fragment amountUser on User {
           amount                          
          }`
      });

      const probabilityMatch = readField('probability');    
      
      const calc = getCalc (amountUser, probabilityMatch); // complex operation
      const maxValue = getMaxValue (calc);
      return maxValue;
    }
   } ​
  ​}}
}

Then query like this:

query Match {
   Match {
     id
     date
     odd @client
     maxValue @client
  }
}

But I'm executing same complex operation twice. An idea would be return nested fields:

const typePolicies = {
 Match {
  fields: {
   computed: {
    read (_ , { readField })  {
      
      const amountUser = cache.readFragment({
        id: 'User:{}',
        fragment: gql`
          fragment amountUser on User {
           amount                          
          }`
      });

      const probabilityMatch = readField('probability');    
      
      const calc = getCalc (amountUser, probabilityMatch); // complex operation
      const odd = getOdd (calc);
      const maxValue = getMaxValue (calc);
      return {
         odd,
         maxValue
      }
     }
    }} ​
  ​}}
}

Then query like this:

query Match {
   Match {
     id
     date
     computed @client {
      odd
      maxValue
     }
  }
}

But I'm creating an type computed that I don't want. Is there an optimal way to solve this?

@jdmoliner jdmoliner changed the title Apollo cache: reuse same derived data in different field Apollo cache: reuse same derived data between different fields Apr 26, 2021
@benjamn
Copy link
Member

benjamn commented Apr 26, 2021

@jdmoliner Two notes that I hope will get you unstuck, though I imagine you'll have further questions:

  1. You can reuse readField instead of cache.readFragment:
read(_, { readField, cache, toReference }) {
  const userRef = toReference(cache.identify({ __typename: "User", ... }));
  const amountUser = readField("amount", userRef);
  ...
}
  1. There's a handy object called storage in the read function options that you can use for caching:
read(_, { readField, cache, toReference, storage }) {
  // Returned cached storage.odd value, if defined:
  if (storage.odd !== void 0) return storage.odd;
  const userRef = toReference(cache.identify({ __typename: "User", ... }));
  const amountUser = readField("amount", userRef);
  ...
  // Cache the final result:
  return storage.odd = odd;
}

This storage object is private to this field within this specific object, but the same storage object is provided to read and merge and cache.modify functions, so (if you later want to invalidate the storage.odd cache) you can either define a merge function that allows deleting storage.odd (perhaps a bit awkward for a client-only field), or call cache.modify like so:

cache.modify({
  id: cache.identify({ __typename: "Match", ... }),
  fields: {
    odd(value, { storage }) {
      // Clear the cached storage.odd value:
      delete storage.odd;
      // Preserve any existing value:
      return value;
    },
  },
})

Please let me know if I can clarify anything about these somewhat obscure corners of the type/field policies API.

@benjamn
Copy link
Member

benjamn commented Apr 26, 2021

Argh, now that I suggested cache.modify, I'm reminded that it only operates on existing fields in the cache, so the function might not be called for client-only fields like Match.odd.

I'll have to keep thinking about the best way to invalidate storage.odd.

@josedavid2021
Copy link

@benjamn that's just the problem I have now, as I can't clear storage cache in typePolicies, when I try later to make a mutation or cache.modify in my App I get storage cache. Has something occurred to you?

@benjamn benjamn self-assigned this Apr 28, 2021
@benjamn
Copy link
Member

benjamn commented Apr 28, 2021

@josedavid2021 I'm thinking of introducing a new field policy function called finalize to go along with merge and read:

new InMemoryCache({
  typePolicies: {
    SomeType: {
      fields: {
        someField: {
          read(existing, { storage }) {
            if ("cached" in storage) return storage.cached;
            ...
            return storage.cached = computeResult(...);
          },
          // Called just before this field is evicted/deleted/removed from the cache:
          finalize(existing, { storage }) {
            delete storage.cached;
          },
        },
      },
    },
  },
})

What do you think about that?

@josedavid2021
Copy link

@benjamn that would be great!

benjamn added a commit that referenced this issue Apr 28, 2021
@benjamn benjamn added this to the Release 3.4 milestone Apr 28, 2021
benjamn added a commit that referenced this issue Apr 29, 2021
As I described in this comment, though I decided to call the new
function `drop` instead of `finalize`:
#8052 (comment)
@josedavid2021
Copy link

josedavid2021 commented May 5, 2021

@benjamn This is my solution until then:

new InMemoryCache({
  typePolicies: {
    SomeType: {
      fields: {
        someField: {
          read(existing, { storage, variables }) {
            
            if (variables.deleteCache) {
              delete (storage.cache)
              return;              
            }

            if ("cached" in storage) return storage.cached;
            ...
            return storage.cached = computeResult(...);
          }         
        },
        otherField: {
          read(_, { realField}) {
           const someField= realField('someField');
           return someField.cached;           
          }         
        },
        ...
        finalize: {
          read(_, { readField}) {
            readField({ fieldName: 'someField', variables: { deleteCache: true } });
            return true;
          }
        }
      },
    },
  },
})

Then query like this:

query SomeType{
   SomeType{     
     ....
     otherField @client
     ....
     finalize @client # at the end of query
  }
}

How about?

@benjamn
Copy link
Member

benjamn commented May 18, 2021

@josedavid2021 That's very clever, and seems like it should work! However, I hope we can come up with something (along the lines of #8078) so you won't need that finalize @client trick.

@hwillson hwillson modified the milestones: Release 3.4, Release 4.0 Jun 8, 2021
@hwillson hwillson removed this from the v4.0 - Planned milestone Jul 9, 2021
Infinite15 added a commit to Infinite15/Apollo that referenced this issue Aug 15, 2024
As I described in this comment, though I decided to call the new
function `drop` instead of `finalize`:
apollographql/apollo-client#8052 (comment)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants