Optimistic updates with Apollo Graphql

I am trying to figure out how to get an optimistic response when I add a task to my todo list.

ADD_TASK mutation

export const ADD_TASK = gql`
  mutation addTask($task: AddTaskInput!) {
    addTask(input: [$task]) {
      task {
        id
        title
        completed
      }
    }
  }
`;

the add Function

 const addTask = mutation(ADD_TASK);
........
........
  async function add() {
    try {
      await addTask({
        variables: {
          task: {
            title: text,
            completed: false,
            user: { username: user?.email },
          },
        },
        optimisticResponse: {
          addTask: {
            __typename: "mutation",
            task: {
              __typename: "Task",
              id: Math.round(Math.random() * -1000000),
              title: text,
              completed: false,
              user: { username: user?.email },
            },
          },
        },
      });
    } catch (e: any) {
      console.log(e);
    }
    text = "";
  }

I believe my problem lies in how I structured the optimisticResponse. If I get rid of it, everything works as expected, but I am trying to figure out how to get this particular feature of Apollo to work.

I happen to be using svelte with typescript from my repo here, but I imagine it would be the same in react etc.

Any Suggestions?

Thanks
J

Try this:

        optimisticResponse: {
          __typename: "Mutation",
          addTask: {
            __typename: "AddTaskPayload"
            task: {
              __typename: "Task",
              id: Math.round(Math.random() * -1000000),
              title: text,
              completed: false,
              user: {
                __typename: "User"
                username: user?.email
              },
            },
          },
        },

No, it did not work. Still just uses normal subscription…

It seems optimisticResponse updates the cache, but I need to use update to actually update the document like Mutation and Update Cache. If I understand this correctly from the Optimistic UI Apollo Doc…

So, I have this, but no change:

        optimisticResponse: {
          __typename: "Mutation",
          addTask: {
            __typename: "AddTaskPayload",
            task: {
              __typename: "Task",
              id: Math.round(Math.random() * -1000000),
              title: text,
              completed: false,
              user: {
                __typename: "User",
                username: user?.email,
              },
            },
          },
        },
        update(cache, { data: addTask }) {
          const todos: any = cache.readQuery({ query: GET_TASKS });
          cache.writeQuery({
            query: GET_TASKS,
            data: { ...todos, tasks: [...todos.tasks, addTask] },
          });
        },

This doesn’t quite work, but I think I am on the right track…

Any ideas?
J

1 Like

I am not sure right now without running my own test and experimenting. I don’t have time to do that right now, but probably can later this week if no one else chimes in.

1 Like

Oh… Oh… subscription…

Wait, this changes a lot of things. I was thinking we were updating a query, but no, you are trying to update a subscription. Here is your GET_TASKS from your repo for context to this:

export const GET_TASKS = gql`
  subscription {
    queryTask {
      id
      title
      completed
      user {
        username
      }
    }
  }
`;

Right off the bat, this throws some complexity. First of all read this:

Even when this is supported, the following still needs addressed:

  • Your subscription is not named. This would prevent an update cache function from being able to find this subscription to work with the cache (if it were possible). To resolve this, add a name to the subscription:
export const GET_TASKS = gql`
  subscription GET_TASKS {
    queryTask {

If Apollo cache were to support subscriptions, this would allow you to then work with the cache of that with an update function. Note that the syntax of the data in the writeQuery, should match the syntax of the GET_TASKS query (e.g. the property of data is queryTask which is an array of tasks)

// NOTE: this only works for query at this time
        update(cache, { data: addTask }) {
          // addTask would contain a task field that has an array of tasks:
          // example - `addTask = { task: [{ id: "0x2", title: "foo", completed: false, user: { username: "bar" } }] }`
          const { queryTask } = cache.readQuery({ query: GET_TASKS });
          // queryTask would be an array of tasks like the following:
          // `queryTask = [{ id: "0x1", ... }, ...]`
          cache.writeQuery({
            query: GET_TASKS,
            data: { 
              queryTask: [ ...queryTask, ...addTask.task ]
            },
          });
        },

And then the final piece of the puzzle would be solving the id situation. I see from you posted examples that you are just created a random number string for the id. Depending on your configuration in apollo, when updating the cache, you may have it setup to merge subsequent subscription responses instead of overwriting the full response. If this is the case, when the response does come back it will not have the same id that you created. This may cause some issues with creating duplicate entries on the client but only a single source of truth on the database. There might be some ways around this, but nothing that is really perfect here.

Having all of this said, you can work around the open issue by using both a query and a subscription with the same name, and then the add mutation, and subsequent subscription responses will update the query. So you must determine how many workarounds you want to do for this to work, or just wait for the subscription to respond with the new task you added which should happen relatively shortly after you submit your mutation.

Ok, so I got it working (without Subscriptions) but with the id problem:

queries.ts

export const GET_TASKS = gql`
  query GET_TASKS {
    queryTask {
      id
      title
      completed
      user {
        username
      }
    }
  }
`;

tasks

      const newId = Math.round(Math.random() * -1000000);
      await addTask({
        variables: {
          task: {
            title: text,
            completed: false,
            user: { username: user?.email },
          },
        },
        optimisticResponse: {
          __typename: "Mutation",
          addTask: {
            __typename: "AddTaskPayload",
            task: {
              __typename: "Task",
              id: newId,
              title: text,
              completed: false,
              user: {
                __typename: "User",
                username: user?.email,
              },
            },
          },
        },
        update(cache, { data: addTask }: any) {
          const queryTask: any = cache.readQuery({
            query: GET_TASKS,
          });
          cache.writeQuery({
            query: GET_TASKS,
            data: {
              queryTask: [...queryTask.queryTask, addTask.addTask.task],
            },
          });
        },

So now I have the two problems you mentioned:

1.) ID Problem

No idea how to solve this. The document is updated immediately with the cache, it seems to work as intended, then 1 second later it is replaced with undefined However, I refresh and it looks correct with the new task. Definitely an ID problem. Is there a way maybe to calculate what next id should be? Maybe a better way?

2.) Subscriptions

a) Do you know if urql can handle optimistic subscriptions? I don’t want to waste my time debugging if that is the case.

b) Or, do you think this could be done on the front end to prevent extraneous calls to dgraph (subscription + query) ?

Thanks,
J

No. Maybe use an id value that on the next update you can remove and add the real one.

Doesn’t look like it from a brief doc review, but I have never used it. Maybe instead of subscription you can just use query and use polling to get new updates. This may increase network traffic when nothing has changed, but there really isn’t another way right now other than using query and subscription together.

I solved the id problem, I just need to solve the subscription problem.

update(cache, { data: addTask }: any) {
  const queryTask: any = cache.readQuery({
    query: GET_TASKS,
  });
  const task = Array.isArray(addTask.addTask.task)
    ? addTask.addTask.task[0]
    : addTask.addTask.task;
  cache.writeQuery({
    query: GET_TASKS,
    data: {
      queryTask: [...queryTask.queryTask, task],
    },
  });
},

Apparently it it sends a refresh automatically with the new id, however, I just had to capture the result as an array when it is sent back automatically to the cache. Weird.

Subscription

What do you mean by this?

I cannot have two variables with the same name. I don’t mind having to send both a subscription and a query, but I am not sure how to do that.

J

The same operation name not javascript variable name. See if you can use this as a base:

It looks like you can run use Subscription like that in react, but I will see if that can be translated to svelte this weekend.

Thanks,
J