Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Apollo client: Making optimistic updates while creation is still in progress

I want to be able to do updates on an object while it is still being created.

For example: Say I have a to-do list where I can add items with names. I also want to be able to edit names of items.

Now say a user with a slow connection creates an item. In that case I fire off a create item mutation and optimistically update my UI. That works great. So far no problem

Now let's say the create item mutation is taking a bit of time due to a slow network. In that time, the user decides to edit the name of the item they just created. For an ideal experience:

  1. The UI should immediately update with the new name
  2. The new name should eventually be persisted in the server

I can achieve #2 by waiting for the create mutation to finish (so that I can get the item ID), then making an update name mutation. But that means parts of my UI will remain unchanged until the create item mutation returns and the optimistic response of the update name mutation kicks in. This means #1 won't be achieved.

So I'm wondering how can I achieve both #1 and #2 using Apollo client.

Note: I don't want to add spinners or disable editing. I want the app to feel responsive even with a slow connection.

like image 336
priomsrb Avatar asked Nov 06 '18 22:11

priomsrb


People also ask

How do you update the Apollo Client cache after a mutation?

If a mutation updates a single existing entity, Apollo Client can automatically update that entity's value in its cache when the mutation returns. To do so, the mutation must return the id of the modified entity, along with the values of the fields that were modified.

Does useMutation return a promise?

A function that performs an asynchronous task and returns a promise.

What is optimistic response in Apollo?

Optimistic UI is a pattern that you can use to simulate the results of a mutation and update the UI even before receiving a response from the server. Once the response is received from the server, the optimistic result is thrown away and replaced with the actual result.

What does optimistic update mean?

An optimistic UI update is an update in the user interface showing the final result even before the change requested to the server has returned a success. In other words, the UI changes to the result even before receiving a response from the server.


Video Answer


2 Answers

If you have access to the server you can implement upsert operations, and you can reduce all queries to the such one:

mutation {
  upsertTodoItem(
    where: {
      key: $itemKey # Some unique key generated on client
    }
    update: {
      listId: $listId
      text: $itemText
    }
    create: {
      key: $itemKey
      listId: $listId
      text: $itemText
    }
  ) {
    id
    key
  }
}

So you will have a sequence of identical mutations differing only in variables. An optimistic response accordingly, can be configured to this one mutation. On the server you need to check if an item with such a key already exists and create or update an item respectively.

Additionally you might want to use apollo-link-debounce to reduce number of requests when user is typing.

like image 168
Oleg Pro Avatar answered Oct 12 '22 14:10

Oleg Pro


I think the easiest way to achieve your desired effect is to actually drop optimistic updates in favor of managing the component state yourself. I don't have the bandwidth at the moment to write out a complete example, but your basic component structure would look like this:

<ApolloConsumer>
  {(client) => (
    <Mutation mutation={CREATE_MUTATION}>
      {(create) => (
        <Mutation mutation={EDIT_MUTATION}>
          {(edit) => (
            <Form />
          )}
        </Mutation>        
      )}
    </Mutation>
  )}
</ApolloConsumer>

Let's assume we're dealing with just a single field -- name. Your Form component would start out with an initial state of

{ name: '', created: null, updates: null }

Upon submitting, the Form would do something like:

onCreate () {
  this.props.create({ variables: { name: this.state.name } })
    .then(({ data, errors }) => {
      // handle errors whichever way
      this.setState({ created: data.created })
      if (this.state.updates) {
        const id = data.created.id
        this.props.update({ variables: { ...this.state.updates, id } })
      }
    })
    .catch(errorHandler)
}

Then the edit logic looks something like this:

onEdit () {
  if (this.state.created) {
    const id = this.state.created.id
    this.props.update({ variables: { name: this.state.name, id } })
      .then(({ data, errors }) => {
        this.setState({ updates: null })
      })
      .catch(errorHandler)
  } else {
    this.setState({ updates: { name: this.state.name } })
  }
}

In effect, your edit mutation is either triggered immediately when the user submits (since we got a response back from our create mutation already)... or the changes the user makes are persisted and then sent once the create mutation completes.

That's a very rough example, but should give you some idea on how to handle this sort of scenario. The biggest downside is that there's potential for your component state to get out of sync with the cache -- you'll need to ensure you handle errors properly to prevent that.

That also means if you want to use this form for just edits, you'll need to fetch the data out of the cache and then use that to populate your initial state (i.e. this.state.created in the example above). You can use the Query component for that, just make sure you don't render the actual Form component until you have the data prop provided by the Query component.

like image 32
Daniel Rearden Avatar answered Oct 12 '22 14:10

Daniel Rearden