Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to update a paginated list after a mutation?

I have a thread with a list of messages, which is fetched with a GET_THREAD_MESSAGES query. That query is paginated, and depending on if a user has seen the thread before or not might load the first page, last page or only the new messages. (i.e. any of first/after/before/last could be passed with any values)

thread(id: "asdf") {
  messageConnection(after: $after, first: $first, before: $before, last: $last) {
    edges {
      cursor
      node { ...messageInfo }
    }
  }
}

Now I have a sendMessage mutation, which I call and then in the update method of that mutation I want to optimistically add that sent message to the threads messages for a nicer UX. Without pagination, I know I would do that something like:

const data = store.readQuery({
  query: GET_THREAD_MESSAGES,
  variables: { id: message.threadId }
})

data.messageConnection.edges.push(newMessageEdge);

store.writeQuery({
  query: GET_THREAD_MESSAGES,
  variables: { id: message.threadId },
  data,
})

Unfortunately, since I know have pagination the store.readQuery call throws an error saying “It can’t find the field messageConnection of that thread” because the field is now something like messageConnection({ after: 'jfds1223asdhfl', first: 50, before: null, last: null }). The Apollo docs say that one should use the @connection directive in the query to work around that. I've tried to update the query to look something like this:

thread(id: "asdf") {
  messageConnection(...) @connection(key: "messageConnection") {
    edges {
      cursor
      node { ...messageInfo }
    }
  }
}

Unfortunately, when I use that the optimistic update is returned and shown correctly, but as soon as the server returns the actual message that was stored I get an error saying "Missing field cursor in { node: { id: '...', timestamp: '...'", because obviously the message that the server returns is not a MessageConnectionEdge, it's just the node, and thusly doesn't have a cursor field.

How can I tell Apollo to only replace the node of the optimistic response, not the entire edge? Is there another way to work around the original issue maybe?

like image 346
mxstbr Avatar asked Jan 13 '18 16:01

mxstbr


People also ask

How do you update cache after mutation?

The steps we need to update the cache are: Read the data from Apollo cache (we will use the same GET_ITEMS query) Update the list of items pushing our new item. Write the data back to Apollo cache (also referring to the GET_ITEMS query)

How do you update mutations in GraphQL?

Update mutations take filter as an input to select specific objects. You can specify set and remove operations on fields belonging to the filtered objects. It returns the state of the objects after updating. Note Executing an empty remove {} or an empty set{} doesn't have any effect on the update mutation.

What does useMutation return?

When your component renders, useMutation returns a tuple that includes: A mutate function that you can call at any time to execute the mutation. Unlike useQuery , useMutation doesn't execute its operation automatically on render. Instead, you call this mutate function.


2 Answers

Phew, I finally fixed this. The problem turned out not to be in the mutation at all, it was in the subscription that was running parallel for new messages in the thread. (facepalm)

TL;DR: If you have a subscription on the same data as a mutation is changing, make sure to include the same fields in both update methods!

The subscription method looked like this:

subscribeToNewMessages: () => {
  return props.data.subscribeToMore({
    document: subscribeToNewMessages,
    variables: {
      thread: props.ownProps.id,
    },
    updateQuery: (prev, { subscriptionData }) => {
      const newMessage = subscriptionData.data.messageAdded;
      return Object.assign({}, prev, {
        ...prev,
        thread: {
          ...prev.thread,
          messageConnection: {
            ...prev.thread.messageConnection,
            edges: [
              ...prev.thread.messageConnection.edges,
              { node: newMessage, __typename: 'ThreadMessageEdge' },
            ],
          },
        },
      });
    },
  });
},

If you have good eyes, you'll immediately spot the issue: The inserted edge doesn't provide a cursor—which is exactly what Apollo Client is telling us in the warning! The fix was to add a cursor to the inserted edge:

{ node: newMessage, cursor: newMessage.id, __typename: 'ThreadMessageEdge' }

Hope this helps somebody else running into this warning, make sure to triple check both the subscription and the mutation update methods!

like image 113
mxstbr Avatar answered Nov 08 '22 07:11

mxstbr


I'll have a crack at this.

Without seeing the mutation I'll presume that it looks something like the following.

mutation NewMessage($message: String!, $threadId: ID!) {
  sendMessage(message: $message, threadId: $threadId) {
    ...messageInfo
  }
}

If that is the case, it's potentially unreliable to infer where in the connection the message should go. Since cursors are opaque strings we can't be certain that this message should indeed come after the latest message.

Instead I'd try something like the following.

mutation NewMessage(message: String!, threadId: ID!, $after: Cursor, first: Int) {

  sendMessage(message: $message, threadId: $threadId) {
    messageConnection(
      after: $after,
      first: $first,
      before: null,
      last: null
    ) @connection(key: "messageConnection") {
      edges {
        cursor
        node { ...messageInfo }
      }
    }
  }
}

This connection should include the new message and any others that have been added since.

Here is the official documentation on the @connection directive: https://www.apollographql.com/docs/react/advanced/caching/#the-connection-directive

like image 35
imranolas Avatar answered Nov 08 '22 06:11

imranolas