Cache: Incoming fields have missing fields if the response's fields are null

This currentUser query was running too many times.

Types:

type UserDetailsType {
  a: String
  b: String
  c: String
}

type User {
 _id: ID!
  details: UserDetailsType
}

type Query {
  currentUser: User
}

The response always comes as

{
    "__typename": "User",
    "_id": "dDnw7wqhMPMeog2Y7",
    "details" : {
        "a" : "something",
        "b" : null,
        "c" : null
    }
}

But the cache was getting updated weirldy. Sometimes those b, c fields would appear on the cache like "b" : null. Then they would dissappear.

So, I tried to compare the existing and incoming like:

new InMemoryCache({
    typePolicies: {
      User: {
        fields: {
          details: {
            merge(existing, incoming, { mergeObjects }) {
              // ...
              console.log('existing', existing);
              console.log('incoming', incoming);
              // ...
            }
          }
        }
      }
    }
  })

That incoming parameter sometimes has missing fields. if those fields are null!!! Thus, causing the cache to remove those null fields.

But if I enable merge missing fields from incoming don’t get removed from cache.

new InMemoryCache({
    typePolicies: {
      UserDetailsType: {
        merge: true
      }
    }
  })

How do I fix it without manually setting merge: true on every possible fields/types?

Hey @Dulguun_Otgon :wave:

What version of @apollo/client are you using? This looks very similar to this issue which was fixed by this PR in v3.7.11. If you’re on a version < 3.7.11, try upgrading to at least that version and see if that helps.

I’m on version v3.7.14 and webpack module federation. I’ve tried to reproduce this issue with no success.

To clarify, when you say:

I’ve tried to reproduce this issue with no success.

Are you saying everything works as expected? Or that you’re still seeing the problem mentioned above but are having difficulty trying to create a reproduction of the issue?

I meant I failed to reproduce this issue. Seems like it’s not an apollo bug, but a bug from the project I’m working on.

Got it! Thanks for the clarification.

No clue what could be causing incorrect cache updates. There are no cache.updateQuery or cache.writeQuery calls that would update that particular field

Any query/mutation/subscription that would return that type with a fetchPolicy set to something that would write to the cache could be triggering that cache update. It doesn’t have to be an explicit cache.updateQuery or cache.writeQuery call. Check around at the queries that are executing and see if any of them query for overlapping data (i.e. the currentUser field). That might help you narrow down which queries might be competing with each other.

So, any type that has User type field and returns wrong data for it could cause it? That makes sense. So it seems like more of a backend bug in the project that I’m working on.

Yep thats correct! And thats because Apollo uses a normalized cache.

Is it possible to disable normalization for some types on the server side?

For example, I want to disable it inside my schema definition (maybe by using some directive).

Cache normalization is a client-only feature so the server would have no knowledge that data is stored in this way on the client. You can however choose to disable data normalization altogether for certain types where it makes sense by setting the keyFields value to false

new InMemoryCache({
  typePolicies: {
    User: {
      keyFields: false
    }
  }
});

This may or may not make sense for you, but play around with it and see what kind of effect it has. If you haven’t installed it already, I’d highly recommend using the Apollo Client Devtools which will allow you to explore your cache. Take note of how the data is shaped/normalized and how disabling normalization affects how that data is stored.

If you’d prefer not to use dev tools and want to inspect it via code, you can dump the contents of the cache by calling its extract function:

// view the entire contents of the cache
console.log(cache.extract())

Hope this helps!

I was hoping for some directive I could use on the schema definition, so that the client can take the hint and just merge them

Unfortunately something like that doesn’t currently exist. You’d need the merge: true trick you originally mentioned.

I’ve managed to reproduce this behavior. I think it’s a bug. Why update and remove the fields that are not even selected?

Would you be able to provide a snippet of the queries, type policies, and what the cache looks like after the queries are run? I’m not quite sure the behavior you’re seeing otherwise.

For example type schema is:

type UserDetailType {
  a: String
  b: String
}

type User {
  id: ID!
  details: UserDetailType
}

type Post {
 id: ID!
 user: User!
}

type Query {
  currentUser: User
  post: Post
}

If I run this query first

query CurrentUser {
  currentUser {
    id
    details {
      a
      b
    }
  }
}

The cache is correctly set.

If I run this query:

query Post {
  post {
    id
    user {
      id
      details {
        a
      }
    }
  }
}

and the post’s user.id is same as currentUser query’s user, then it removes the details.b field from cache.

After this if I run the currentUser query, it has to send request to the server again. Because the selected details.b field got removed from the cache, after running Post query.

Is UserDetailType a normalized type in the cache? In other words, do you see a { __ref: 'UserDetailType:...' } value for that details field, or the full type? I’m guessing it is not normalized since I don’t see an id for that field in your schema.

The reason its getting replaced is because it doesn’t know how to handle merging that details field on the User type out of the box. By default, the cache assumes that any non-normalized field should be overwritten. The a look at the “Merging non-normalized objects” section in the docs and I think you’ll see that example given is almost identical to this one. You’ll need to define a merge function for that field.

Hope this helps!

When you’re using document databases like MongoDB. A type without an id is very common. Writing merge for every nested type without and id is kinda pain in the ass. Wasn’t expecting this when I migrated from version 2 to version 3.