How to use Optimistic UI in React and Apollo GraphQL

Improve the user experience and responsiveness of your React applications with optimism !

French version : Utiliser l'Optimistic UI avec React et Apollo GraphQL

Goals

  • Discover the principle of optimistic UI
  • Use it with React and GraphQL to increase user experience

Context

The response time between the backend and the frontend is one of the factors limiting the user experience. This is even more true when the application or the website is intended for mobiles which may have a degraded connection.

As soon as a user performs an action to send data to the server, this is called a mutation, the client application waits for the return of the server to update the interface. If the server response time is importantly, the user experience is bad.

anim1

A bit of theory

The application manages a list of characters, we want to be able to add characters. The characters have a unique ID, and a Username. The network is very slow.

The user wants to add a character, he must enter his Username and click on a registration button. A mutation is sent to the server which will create the character, generate its ID and return an updated list of characters.

Here is a diagram of the actions, when all goes well :

schema_optimistic1

There could possibly be a problem on the server side, and the creation of the new character may not go as planned. In this case, the diagram might look like this :

schema_optimistic2

The optimistic UI is simply the fact of placing oneself in the event that everything goes as planned: the client ignores this that happens on the server side at first, it assumes that everything is going to be fine. In a second time it retrieves information from the server, updates the properties that were generated by the server (here the ID), and in case of error it returns to the initial state.

schema_optimistic3

For the user, his action is felt to be instantaneous.

Start the project

To put our example into practice, we need a client application (in React), a server (in Node.js), both connected by an API (GraphQL). On the client side, you will need cache or state management, Apollo GraphQL does it perfectly.

We use create-react-app to bootstrap the client application with a basic configuration.

The server is a classic Apollo Graphql server, with express middleware that will simulate a response time of 1.5 seconds:

// server.js - Add some big lag
app.use((req, res, next) => {
  setTimeout(() => next(), 1500);
});

Client application

Our React component displays a list of characters, we get the list via a GraphQL query. We then pass the list to the compoonent who will display it.

// UsersWithData
const queryRequest = gql`
  query {
    users {
      id
      username
    }
  }
`;

function UsersWithData() {
  // Thanks to Apollo Hooks
  const { data, loading } = useQuery(queryRequest);

  // Inform user data is coming
  if (loading)
    return (
      <main>
        <div>loading...</div>
      </main>
    );

  return <Users users={data.users} />;
}

The main component displays the list and a field that allows you to enter the name of the new character :

// Users
function Users({ users }) {
  const [username, setUsername] = useState("");

  const changeUsername = (e) => {
    setUsername(e.target.value);
  };

  return (
    <main>
      <div>
        Username :
        <input type={"text"} value={username} onChange={changeUsername} />
        <button onClick={handleCreateUser}>add</button>
        {users.map((user) => (
          <div key={user.id}>
            <div>{user.username}</div>
            <button>X</button>
          </div>
        ))}
      </div>
    </main>
  );
}

We add the GraphQL mutation using the useMutation hook provided by Apollo :

// Users
const mutationRequest = gql`
  mutation($username: String!) {
    createUser(username: $username) {
      id
      username
    }
  }
`;

function Users({ users }) {
  const [username, setUsername] = useState("");
  const [createUser] = useMutation(mutationRequest);

  const handleCreateUser = () => {
    createUser({
      variables: { username },
      refetchQueries: [{ query: queryRequest }],
    }).catch((err) => err);
  };

  const changeUsername = (e) => {
    setUsername(e.target.value);
  };

  return (
    <main>
      <div>
        Username :
        <input type={"text"} value={username} onChange={changeUsername} />
        <button onClick={handleCreateUser}>add</button>
        {users.map((user) => (
          <div key={user.id}>
            <div>{user.username}</div>
            <button>X</button>
          </div>
        ))}
      </div>
    </main>
  );
}

The application is functional and allows you to add a character and retrieve the updated list from the server. The list refetch operation takes time and leaves an unpleasant feeling to the user !

Let's be optimistic !

To add optimistic UI to our application, we just need to change the data refetch strategy when of the mutation. In the previous mutation, a query was passed as a parameter via the "refetchQueries" property, which told GraphQL to re-execute this query after the mutation and update the cache.

To use Apollo's optimistic UI, you have access to two properties: "optimisticResponse" and "update".

The first property, "optimisticResponse" manages the entity that we create, here the character, the property "update" defines how to update our local cache with this new entity. This update will take place without the user noticing it, upon receipt of the response from the server.

const handleCreateUser = () => {
  createUser({
    variables: { username },

    optimisticResponse: {
      __typename: "Mutation",
      // use the uuid library to have a unique ID
      createUser: { __typename: "User", id: 123456, username },
    },

    update: (proxy, { data: { createUser } }) => {
      // Get the data from GraphQL cache
      const data = proxy.readQuery({ query: queryRequest });
      // Update the cache with the query
      proxy.writeQuery({
        query: queryRequest,
        data: { ...data, users: [...data.users, createUser] },
      });
    },
  }).catch((err) => err);
  setUsername("");
};

Here are the two components used, one with Optimistic UI, the other without :

anim2

You can find all the sources here.