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

[cacheRedirects/typePolicies] Retrieving fragments from the cache and returning an array of references #6278

Closed
darkbasic opened this issue May 14, 2020 · 6 comments
Assignees
Milestone

Comments

@darkbasic
Copy link

typePolicies are really easy if you just fetched a list of Chats and then want to query one of them:

typePolicies: {
  Query: {
    fields: {
      chat(
        existingDataFromCache: Reference | undefined,
        {args, toReference},
      ) {
        return (
          existingDataFromCache ||
          (args?.chatId &&
            toReference({
              __typename: 'Chat',
              id: args.chatId,
            }))
        );
      },

But what if you fetched a Chat along with all its messages

chat(chatId: ID!): {
  id,
  [...],
  messages: {
    id,
    [...]
  }
}

and now you want to query all messages for that given chat?

The first thing which came to my mind was something like this:

import ChatFragment from '@fragments/Chat.fragment.graphql';

export const client = new ApolloClient({
  cache: new InMemoryCache({
    typePolicies: {
      Query: {
        fields: {
          messages(
            existingDataFromCache: Reference | undefined,
            {args, toReference},
          ) {
            if (!existingDataFromCache && args) {
              const chat = client.readFragment({
                id: args.id,
                fragment: ChatFragment,
                fragmentName: 'Chat',
              });
              if (chat) {
                return chat.messages.map(({id}) =>
                  toReference({
                    __typename: 'Message',
                    id,
                  }),
                );
              }
            }
            return existingDataFromCache;
          },
        },
      },
    },
  }),

But unfortunately:

  1. There is no easy way to access the client/cache from within typePolicies, which leads me to think that I shouldn't be supposed to do so. The workaround is to access it through the variable name I used to instantiate the Apollo client.
  2. Calling readFragment from within typePolicies always returns null. Doing so from a React Component works flawlessly.

So, is something like this even supported? I hope so, because otherwise the whole cacheRedirects/typePolicies thing would be pretty useless.
I'm using @apollo/[email protected].

@benjamn benjamn added this to the Release 3.0 milestone May 14, 2020
@benjamn
Copy link
Member

benjamn commented May 14, 2020

I think the answer here is the readField helper, which can optionally take a Reference as its second argument:

new InMemoryCache({
  typePolicies: {
    Query: {
      fields: {
        messages(_, { args, toReference, readField }) {
          return readField("messages", toReference({
            __typename: "Chat",
            id: args.id,
          }));
        },
      },
    },
  },
})

If you pass only a field name to readField, it assumes you want to read from the current object.

@darkbasic
Copy link
Author

Thanks @benjamn for the quick answer. Unfortunately readField always returns undefined. It works for simple scalar fields, but not for messages. I probably over-simplified my previous example, but messages uses Relay-style cursor pagination:

fragment GroupChatExtended on GroupChat {
  ...GroupChatBase

  messages(last: $last, before: $before) {
    ...MessageOrMembershipConnection
  }

  [...]
}

And it looks something like this:

{
  "data": {
    "messages": {
      "count": 40,
      "edges": [
        {
          "cursor": "2020-04-27T06:12:27.000Z",
          "node": {
            [...],
            "__typename": "Message",
          },
          "__typename": "MessageOrMembershipEdge"
        },
        {
          "cursor": "2020-04-27T06:00:00.000Z",
          "node": {
            [...],
            "__typename": "JoinMembership",
          },
          "__typename": "MessageOrMembershipEdge"
        },
        {
          "cursor": "2020-04-27T06:00:00.000Z",
          "node": {
            [...],
            "__typename": "LeaveMembership",
          },
          "__typename": "MessageOrMembershipEdge"
        }
      ],
      "pageInfo": {
        "startCursor": "2020-04-27T06:12:27.000Z",
        "endCursor": "2020-04-27T06:00:00.000Z",
        "hasNextPage": false,
        "hasPreviousPage": true,
        "__typename": "PageInfo"
      },
      "__typename": "MessageOrMembershipConnection"
    }
  }
}

I'm not sure if readField is supposed to behave that way (I wasn't even sure if it was the right tool for the job), but if at any point you feel like I'm doing the right thing but the outcome is unexpected please let me know and I'll provide a repro.

@benjamn
Copy link
Member

benjamn commented May 27, 2020

@darkbasic With PR #6306 (included in -beta.50), you should be able to call readField with arguments like so:

new InMemoryCache({
  typePolicies: {
    Query: {
      fields: {
        messages(_, { args, toReference, readField }) {
          return readField({
            fieldName: "messages",
            args: { last, before },
            from: toReference({
              __typename: "Chat",
              id: args.id,
            }),
          });
        },
      },
    },
  },
})

Let me know if you run into any problems with that style in your real application, since I definitely intend for readField to be used in situations like this.

@darkbasic
Copy link
Author

Thanks @benjamn I tried beta.52 and it works fine! To everyone reading please note that toReference may actually return undefined so it must be type checked before actually being used inside readField like in the example before.

@bryan-gislason
Copy link

Hey @darkbasic and @benjamn. For the same example above, how would one add the ability to read a message by id from the cache as opposed to getting the list of messages? I have a similar situation, so I think the example above works for the purposes of my question.

Since readFragment doesn't work in the typePolicies, is there another direct way of getting the message without using readField('messages') and having to iterate through the messages looking for the one with the matching id?

new InMemoryCache({
  typePolicies: {
    Query: {
      fields: {
        readMessageById(_, { args, toReference, readField }) { // define custom local field
           const messages = readField({
            fieldName: "messages",
            args: { last, before },
            from: toReference({
              __typename: "Chat",
              id: args.id,
            }),
          });
          return messages?.find({id} => args.messageId);
          });
        },
      },
    },
  },
})

@bryan-gislason
Copy link

I've resolved to using readFragment outside of typePolicies for this scenario which works fine and seems like the proper way to do this.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Feb 15, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants