How to get the old item value before it is overwritten by the subscription?

I have a subscription which returns me some item, which I can access in the onData callback. The problem is, this data is already from the updated item, but I need to also access the old one at the same time to compare them. I can’t use cache.readQuery, because the data is already updated in the cache, and I can’t also get it from merge function in typePolicies due to this bug: Apollo Cache V3: References instead of items within merge · Issue #5876 · apollographql/apollo-client · GitHub

The question, how can I do that? I just need to compare the old value of the field with the new one, it have to be very simple, but i’ve spent many hours already and I still have no idea how to do that

Hey @weqwwewqweq :wave:

A type policy is your best bet in this case. That issue you linked is actually not a bug! If you’re getting a Reference in your merge function, you can use the readField function given to you in the third argument to your merge function in order to get fields from the referenced object. You can see this in the FieldPolicy API reference.

new InMemoryCache({
  typePolicies: {
    MyType: {
      fields: {
        myField: {
          merge(existing, incoming, { readField }) {
            const oldField = readField('someField', existing)
            const newField = readField('someField', incoming)
            // do something with these fields
          }
        }
      }
    }
  }
})

You can see some additional examples of the readField helper in the merge function doc. Hope this helps!

first of all, since it’s not real items and just references - both existing and incoming objects are references to the same object in the cache:

        merge: (existing, incoming, { mergeObjects, readField }) => {
          const oldField = readField('messageCount', existing);
          const newField = readField('messageCount', incoming);

          console.log(oldField === newField); // true all the time, even if messageCount changes all the time
          return mergeObjects(existing, incoming);
        },

second thing - if both are references, merging them becomes kinda pointless. what’s the purpose of merging function, where both existing and incoming are references to the same object? I can just discard existing and return incoming, nothing would change.

so maybe it’s not a bug and just a poor design choice, but anyway, it makes things extremely hard (if possible) to use this way.

Would you be willing to give me a better picture of your schema and the changes you’re trying to make? It would help me more easily determine if this is a bug or if our documentation just isn’t clear on how merge functions work. Thanks!

The relevant part of the schema:


type Chat {
  id: ID!
  messageCount: Int!
  readMessages: Int!
  lastReceivedMessage: String
}

type Subscription {
  onMessage: Chat!
}

subscription:

subscription onMessage {
  onMessage {
    id
    messageCount
    readMessages
    lastReceivedMessage
  }
}

I also have a global message counter, aggregate from all of the chats, which i need to update on such kind of events (the onMessage subscription is just one of many). So, in order to do it, I need to know how messageCount and readMessages are changed by each subscription event. I don’t care if it will be in merge function, onData callback for a subscription or any other place - I just need one place in the code, where I would be able to get an old chat and an updated chat, and run some logic, based on the difference between them. In redux or something similar it would be pretty trivial, but in case of Apollo i just struggle to find where it could be done

After discussion with the team, it looks like this might actually be a limitation of the way the cache is currently implemented. Lenz recently opened an issue very similar in nature here (No access to fields on old an new value in a typePolicy.merge function · Issue #11221 · apollographql/apollo-client · GitHub). This is definitely something we want to address in a future version of the client to make this easier.

For now, I have a couple suggestions to help you move forward. I don’t entirely know how you’ve set everything up, but hopefully this gives you some ideas to make this work well enough for your use case.

  1. Add a merge function to the root subscription onMessage field.

merge functions are called relative to their field in the operation you’re executing. Here because the data is returned from your root subscription operation, the data is written to the cache via the onMessage field first. This might be an opportunity to intercept that object before its written to the cache, you just have to do it at a different point in the type policy than expected.

The caveat here is that we don’t cache root subscription fields by default, so to read the existing data in the cache, you might need to use something like cache.readFragment() to get the data you need.

See if something like this works for you (warning untested):

new InMemoryCache({
  typePolicies: {
    Subscription: {
      fields: {
        onMessage: {
          merge: (_, incoming, { cache }) => {
            const existing = cache.readFragment({
              id: cache.identify(incoming) // assuming `incoming` has __typename and id fields on it
              fragment: gql`
                fragment ChatRecord on Chat {
                  messageCount
                  # other fields you need access to
                }
              `
            })

            // do stuff with existing + incoming here
          }
        }
      }
    }
  }
})
  1. Use a ref in the same component you have your useSubscription call to keep track of the last message or set of messages.

If the above doesn’t work for you, or if you’re still having troubles, you might be able to keep track of the previous value yourself using a ref. Less-than-ideal but might still work for your needs.

function MyComponent() {
  const lastMessageRef = useRef()

  useSubcription(subscription, {
    onData: ({ data }) => {
      const prev = lastMessageRef.current;
      // do something with prev + new data
      lastMessageRef.current = data;
    }
  })
}

Hopefully one of these two options work for you!

We definitely realize this is a major shortcoming of the current API and not the first time we’ve seen this type of thing asked for. Again, this is something on our radar that we plan to address at some point in a major version where we can make some breaking changes to make this a bit more intuitive.

Well, your solution with subscription level type policies doesn’t work either due to the same reasons - existing contains already updated data, and not the old one. It seems like cache entry for Chat updates before merge function for subscription runs, so no luck here either.

As for the workaround with the ref, I’m not sure how to implement it due to the fact that Chat data may come from multiple sources, like other subscriptions or queries/mutations, so it must be not just a ref, but some sort of global storage, which I have to update on every query/mutation/subscription. Feels like I just need to reimplement apollo cache in some redux or just a plain react context, but still it feels like a lot of work for such a simple use case. No idea if there is a better workaround exists, but chatgpt suggests me to do the exact same thing with refs :slight_smile:

BTW, i’ve also tried one more thing, and it looks like it works, but it feels like i just rely on some bug or something like that, so I afraid to use it in prod. My solution:

      Chat: {
        fields: {
          messageCount: {
            merge: (existing, incoming) => {
              // here existing !== incoming
              return incoming;
            },
          },
        },
      },

why do i think it’s a bug? well, i’ve tried this solution before and it didn’t work, existing was always equal to incoming, so maybe i messed up with some other field policies somewhere or with some other apollo settings. But if it turns out that actually i messed up before, but now it works as intended, that would probably enough for my case

Ah yes, we run the merge functions from inside out on types, so we’d run messageCount before we run any fields that return a Chat object. For some reason I was thinking you’d need the whole Chat to work with what you’re doing, but glad this is working for you now!

Interesting this didn’t work before. I do think we need some better documentation that more thoroughly explains the order in which merge functions run since I think that would clear up some confusion (especially around how existing is set). Perhaps you just had the right combination of things, or we fixed a bug in some newer version that prevented this from working before. Regardless, glad this is working for you now.

We’ve got some plans for changing how the cache merge functions work in a future major version of the client, but for v3, this behavior should be stable (and if its not, please report a bug so we can look into it!).

You are totally right, I need more than just 1 field, but that’s not a problem - I can just set all individual fields to some variable object above the cache declaration and run my handler from the merge function of the Chat type policy - at that moment of time all individual fields would be already saved to that object. Sure, that’s a dirty workaround, but at least it’s just in one file and one place, still much easier to implement and maintain it than having a redux-like storage for all the chats and updating it manually on every single query/mutation/subscription (i have tens of them!) everywhere across the app. So as i said, for my case that’s a good enough workaround. Thx for helping me to understand the possibilities and limitations of the cache, now I can continue working on :slight_smile:

1 Like

You’re welcome! Glad you have a path forward :slightly_smiling_face: