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

How do I distinguish between two queries with the same __typename to get Apollo Client typePolicies cache to work properly #9192

Open
meesfrenkelfrank opened this issue Dec 12, 2021 · 4 comments

Comments

@meesfrenkelfrank
Copy link

I have two GraphQL queries from our GraphQL server (see below), but they both have the same __typename: "__typename": "RelatedNodes",.

I am building a Carousel with pagination using Apollo Client fetchMore function. When I use just one query all works fine.

But when I use both queries, Apollo Client cache gets confused.

The Apollo Client typePolicies configured like:

typePolicies: {
    RelatedNodes: {
      keyFields: [],
      fields: {
        nodes: offsetLimitPagination(),
      },
    },
  },

Query one:

query GetNodesTypeOne($limit: Int, $offset: Int,) {
  getNodesTypeOne(limit: $limit, offset: $offset) {
    __typename
    nodes {
      uuid
      type
      title
    }
  }
}

Which returns:

{
  "data": {
    "getNodesTypeOne": {
      "__typename": "RelatedNodes",
      "nodes": [
        {
          "uuid": "ac6ea",
          "type": "TypeOne",
          "title": "Foo"
        },
        {
          "uuid": "7768",
          "type": "TypeOne",
          "title": "Foo bar"
        },
        {
          "uuid": "1e07e",
          "type": "TypeOne",
          "title": "Foooo"
        },
      ]
    }
  }
}

And query two:

query GetNodesTypeTwo($limit: Int, $offset: Int,) {
  getNodesTypeTwo(limit: $limit, offset: $offset) {
    __typename
    nodes {
      uuid
      type
      title
    }
  }
}

Which returns:

{
  "data": {
    "getNodesTypeTwo": {
      "__typename": "RelatedNodes",
      "nodes": [
        {
          "uuid": "76676",
          "type": "TypeTwo",
          "title": "Foo"
        },
        {
          "uuid": "89896",
          "type": "TypeTwo",
          "title": "Foo bar"
        },
        {
          "uuid": "45215",
          "type": "TypeTwo",
          "title": "Foooo"
        },
      ]
    }
  }
}

How do I have to set up my typePolicies so that the cache can distinguish between the two queries? Or do I have to change the GraphQL server types?

@benjamn
Copy link
Member

benjamn commented Dec 13, 2021

@meesfrenkelfrank Though there are a few different ways to approach this kind of problem, I'll focus on your keyFields: [] configuration, because I think that's the main source of complexity/confusion.

First of all, just in case you weren't aware, defining keyFields: [] in a type policy means all objects of that type have the same fixed/empty set of primary key fields, so there is effectively only one singleton object of type RelatedNodes.

However, based on your description, it sounds like you have not just one RelatedNodes object but many different objects, each containing their own independent nodes arrays? To my way of thinking, this means the singleton configuration keyFields: [] must not be quite right.

Putting RelatedNodes type information in the keyFields array

Now, if it was possible to provide a unique identifier for each RelatedNodes object, like an id field or maybe a type field, you could put that field in your keyFields: [...] array, and then you'd get a distinct RelatedNodes object for each different type (one, two, etc):

new InMemoryCache({
  typePolicies: {
    RelatedNodes: {
      keyFields: ["type"],
      fields: {
        nodes: offsetLimitPagination(),
      },
    },
  },
})

And then make sure to request the type field in your queries:

query GetNodesTypeOne($limit: Int, $offset: Int,) {
  getNodesTypeOne(limit: $limit, offset: $offset) {
    __typename # "RelatedNodes"
    type # assume server returns "one"
    nodes {
      uuid
      type
      title
    }
  }
}

An approach like this will keep the different nodes arrays separate in the way you want (based on the requested type), because the RelatedNodes objects are normalized separately according to their __typename and type.

However, this requires RelatedNodes to have a type field defined in your schema, and you have to request that type field in your queries. To help enforce this expectation, with keyFields: ["type"] configured, InMemoryCache will throw an exception when writing RelatedNodes objects if they do not have a type field.

Disabling normalization for RelatedNodes objects

If you don't have anything useful to put in the keyFields array for RelatedNodes, there's another option.

Specifying keyFields: false in a type policy disables normalization for that type, so your RelatedNodes objects will not be normalized at all, but will be stored directly as values (not References) under their parent fields Query.getNodesTypeOne and Query.getNodesTypeTwo, effectively separating RelatedNodes objects from each other according to their parent field.

Because the cache no longer knows the identities of RelatedNodes objects, it can't safely merge them together, so if you request getNodesTypeOne multiple times, each new object will replace/clobber the previous one, which is probably not what you want. This brings us to my final suggestion:

new InMemoryCache({
  typePolicies: {
    RelatedNodes: {
      // Prevents normalization for RelatedNodes objects
      keyFields: false,
      // Allows RelatedNodes objects to be merged, rather than newer objects replacing older
      merge: true,
      fields: {
        // This field policy should continue to work as it did before
        nodes: offsetLimitPagination(),
      },
    },
  },
})

Adding merge: true (#7070) gives the cache permission to merge RelatedNodes objects together, triggering your field policy for the RelatedNodes.nodes field—but only for RelatedNodes objects stored under the same parent field. It's as if the identity of each RelatedNodes object is now inherited from (or determined by) its parent field, rather than coming from the RelatedNodes object itself.

Closing thoughts

If there's some field on RelatedNodes that you can include in keyFields: [...] to give each RelatedNodes a distinct stable identity, I believe that's the simplest solution here (the first approach). I would avoid using the keyFields: false and merge: true strategy (the second approach) if there's any useful notion of RelatedNodes identity you can encode in keyFields, though keyFields: false can be a useful for objects that truly do not have their own identities.

Happy to keep brainstorming if neither of the two approaches I outlined above works for you!

@meesfrenkelfrank
Copy link
Author

meesfrenkelfrank commented Dec 13, 2021

@benjamn thanks a lot for your clear explanation! I'll discuss if solution one is possible (to update the schema with a type field).
Also tried second solution. But somehow now the array isn't merged. So in ui I can't navigate any further (in the Carousel in my case)? What could be the issue?

When I debug below, in the console, existing array is now empty?

   nodes: {
      merge(existing = [], incoming) {
        console.log('Existing', existing);
        console.log('Incomming', incoming);
        return [...existing, ...incoming];
      },
    },

Also I tried a third option, but I don't know if thats possible?

Another option which I am going to discuss is to replace RelatedNodes with two new objects: RelatedNodesOne and RelatedNodesTwo.

Then my typePolicies could be:

typePolicies: {
    RelatedNodesOne: {
      keyFields: [],
      fields: {
        nodes: offsetLimitPagination(),
      },
    },
    RelatedNodesTwo: {
      keyFields: [],
      fields: {
        nodes: offsetLimitPagination(),
      },
    },
  },

Is that correct? What would be the best option to change the schemas in your opinion. Adding two different object to distinguish or adding a type field to the original object RelatedNodes, like you suggested above?

@meesfrenkelfrank
Copy link
Author

meesfrenkelfrank commented Jan 12, 2022

@benjamn I need your help. Backend has been changed and now I have two singleton objects RelatedNodesOne and RelatedNodesTwo. However, those objects doesn't has an id field.

My typePolicies are configured like this now:

typePolicies: {
    RelatedNodesOne: {
      keyFields: [],
      fields: {
        nodes: offsetLimitPagination(),
      },
    },
    RelatedNodesTwo: {
      keyFields: [],
      fields: {
        nodes: offsetLimitPagination(),
      },
    },
  },

The issue I am facing is the following also described here:

When navigating from the overview page to a detail node page it's rendered (and merged) correctly. Query and fetchMore is working fine.

Now when pressing the browser back button to overview page and from there navigate to another node the results from the first cache are presented (merged) with the new ones from this current node? So the content/results are mixed up now.

What I also tried is the following:

typePolicies: {
    RelatedNodesOne: {
      keyFields: false,
      merge: true,
      fields: {
        nodes: offsetLimitPagination(),
      },
    },
    RelatedNodesTwo: {
      keyFields: false,
      merge: true,
      fields: {
        nodes: offsetLimitPagination(),
      },
    },
  },

But then the fetchMore isn't working anymore, looks like existing and incoming are not merged correctly?

Whats causing this issue and how to solve it? Many thanks!

@liorpevzner
Copy link

I have a related question:

I have two different queries that both return the an object with the same key, and typeName but the value is different.
apollo cache overrides those queries.
I tried following this thread, but I got confused and it wasn't exactly the same:
#9192

so my firstQuery returns:

{
"data": {
"getCurrent": {
{....some object"},
"__typename": "Customer"
}
}
}
the second query returns:

{
"data": {
"getCurrent": {
{... different object}
},
"__typename": "Customer"
}
}
}
im trying to define the typePolicies correctly...
so I can do:

Customer:{
keyFields:[?],
}
Can anyone point me to the right direction?

some object/different object are both partials to the getCurrent query.
the first query is for field1,field2, and the second query is for field3/field4.

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

3 participants