DEV Community

welschmoor
welschmoor

Posted on

Optimistic UI with GraphQL in Apollo

A short guide on how to update the UI before the mutation.

On a typical website when you click a button, such as the like button or the follow button, it usually takes a noticeable amount of time for the change to register. It makes the app slow and unresponsive. So, how do we design the app in such a way that it updates the UI instantly?

There are many ways, such as the optimistic response mutation. But if the mutation resolver does not send you back the thing that you have updated, you're out of luck (unless you know something I don't). So, if you can't use Apollos optimistic ui for some reason, this is the guide for you.

I've only been programming for 10 months, so take everything with caution. But here's a way of how I made my UI blazingly fast.

THE FIRST WAY
The first way is for times when there are no lists. Instead of using data from Apollo's cache { data } = useQuery(), we create a state and use that instead. When we for example click a Like button, we update that state and instantly render the result on to the screen without waiting for the server to respond. To keep the data in sync, we use the onCompleted property from the query. A short guide:

(1) Create useStates. One for the case that you have already liked the picture and one for the count of all likes for a given post or photo.

  const [isLikedByMeST, setIsLikedByMeST] = useState(false)
  const [numberOfLikes, setNumberOfLikes] = useState(0)
Enter fullscreen mode Exit fullscreen mode

(2) Use the onCompleted function of the useQuery to set this state to whatever is in the DB

  const { loading, data } = useQuery(SEE_PIC, {
    variables: { seePhotoId: Number(id) },
    onCompleted: (completedData) => {
      setIsLikedByMeST(completedData.seePhoto.isLikedByMe)
      setNumberOfLikes(completedData.seePhoto.likes)
    }
  })
Enter fullscreen mode Exit fullscreen mode

(3) Increase the number of likes directly per setState. I use letter p to indicate the previous state. Since it's a Boolean, we return the opposite after a click: !p

  const likeHandler = async (id) => {
    setIsLikedByMeST(p => !p)
    setNumberOfLikes(p => {
      if (isLikedByMeST) {
        return p - 1
      }
      return p + 1
    })
    await toggleLike({ variables: { id: id } })
  }
Enter fullscreen mode Exit fullscreen mode

(4) Use this new state to show in the UI instead of the data from cache.

<Likes >
  {numberOfLikes === 1 ? "1 like" : `${numberOfLikes} likes`} 
</Likes>

Enter fullscreen mode Exit fullscreen mode

Now after every mouse click on that like button the UI is updated instantly.

THE SECOND WAY
This is the easiest way: When you don't have a list, you can use the {loading} property of the query to update the UI. Instead of waiting for UI to change you change it right as loading becomes true. The idea came to me as I was watching this video by Ryan Florence.

const [toggleLike, { loading: likeLoading }] = useMutation(TOGGLE_LIKE, { //...
Enter fullscreen mode Exit fullscreen mode

As likeLoading becomes true, simply register the like on to UI aaaand you're done!

THE THIRD WAY (for lists)
Now we will have to do more work. We can't use {loading} in a list, because that would update EVERY item in our list and your UI will flare up with changes for all items even though you have only liked one.

Here we will fetch all users and see if we follow them or not. We want to be able to click on the follow button and see UI change instantly without waiting for server to respond.

(1) Create a state array where we will store the information we need

  const [fastUpdateST, setFastUpdateST] = useState([])
Enter fullscreen mode Exit fullscreen mode

(2) Use onCompleted to populate our state with necessary information: The ID of the user and the Boolean if we follow them or not. The fastUpdateST becomes an array of objects where we associate the id from the users list with the property isFollowing: [{id: 5, isFollowing: true}, {id: 6, isFollowing: false}, ... ]

  const { data: allUsersData, loading: loadingData } = useQuery(SHOW_ALL_USERS, {
    variables: { limit: 10 }, // show only 10 users
    fetchPolicy: "cache-and-network",
    nextFetchPolicy: "cache-and-network",
    onCompleted: completedData => {
      setFastUpdateST(
        allUsersData.showAllUsers.map(e => {
          return { id: e.id, isFollowing: e.isFollowing }
        })
      )
    }
  })
Enter fullscreen mode Exit fullscreen mode

(3) Let's handle follow - the button handler for the follow button - where we first update the UI and only then do the mutation. newState is simply like the old state, but the Boolean isFollowing for that particular user is the !opposite

  const followUserHadler = async (username, userid) => {
    setFastUpdateST(p => {
      const newState = [...p.filter(e => e.id !== userid), { id: userid, isFollowing: !p.find(e => e.id === userid).isFollowing }]
      return newState
    })

    await followUser({
      variables: { username: username },      
    })
  }
Enter fullscreen mode Exit fullscreen mode

(4) And here's the JSX where we conditionally render either a checkmark (that we already follow the user) or the word follow, if we yet don't. All this code below is inside a .map(e => e.id .... and runs for every item in a list of users.
I use the letter e to mean each. So, we render the isFollowing Boolean from fastUpdate state and not from cache data!

I also added the { loading: followLoading } from both useMutations to temporarily disable the follow button while it is loading to avoid quick, accidental double clicks.

                  <FollowBTN disabled={unfollowLoading || followLoading} onClick={e.isFollowing ? () => unfollowUserHadler(e.username, e.id) : () => followUserHadler(e.username, e.id)}>
                    {fastUpdateST.length > 0 && fastUpdateST.find(each => each.id === e.id).isFollowing ? <CheckMarkIcon /> : "Follow"}
                  </FollowBTN>
Enter fullscreen mode Exit fullscreen mode

Congratulations! Your app is now as fast as a desktop app. If you know a better way of achieving that, please let me know!

The full code can be seen here: CLICK

Top comments (0)