Fetching Data In React Apps And Optimistic UI Updates Using SWR 2.0

Fetching Data In React Apps And Optimistic UI Updates Using SWR 2.0

Discover how SWR 2.0 simplifies data fetching and enables Optimistic UI Updates and more such features in React applications.

So, In my last blog, I talked about how we can fetch data and perform CRUD operations on API using Axios. There was an imminent problem with Axios. It needed a page refresh on any CRUD operation to show the mutated data.

While this is OK with Vanilla JavaScript Websites, but can widely hamper the experience of Single Page Applications(SPA). We can in turn use the useEffect hook in react to show changes by re-rendering the component on every CRUD operation. But this in turn can be inefficient and can have a bad Developer Experience(DX) as we would have to write the same useEffect code again and again for every change.

SWR (Stale-While-Revalidate)

As stated in the official Docs, SWR is a strategy to first return the data from the cache (stale), then send the fetch request (revalidate), and finally, come up with the up-to-date data. The new SWR 2.0 update comes packed with features such as:-

  • New Mutation API

  • Optimistic UI Updates

  • Preloading Data and many more...

In this blog article, We are primarily going to deal with the Mutation API and Optimistic UI Updates offered by SWR 2.0.

useSWRMutation Hook

In SWR 2.0, the new hook useSWRMutation makes it even simpler to remotely change data using a declarative API. You can set up a mutation using the hook, and then activate it later:

import useSWRMutation from 'swr/mutation'

async function sendRequest(url, { arg }) {
  return fetch(url, {
    method: 'POST',
    body: JSON.stringify(arg)
  })
}

function App() {
  const { trigger, isMutating } = useSWRMutation('/api/user', sendRequest)

  return (
    <button
      disabled={isMutating}
      onClick={() => trigger({ username: 'johndoe' })}
    >{
      isMutating ? 'Creating...' : 'Create User'
    }</button>
  )
}

The example above defines a sendRequest mutation that affects the '/api/user' resource. Unlike useSWR, useSWRMutation will not immediately start the request upon rendering. Instead, it returns a trigger function that can later be called to manually start the mutation.

The sendRequest function will be called when the button is clicked, with the extra argument { username: 'johndoe' }. The value of isMutating will be set to true until the mutation has finished.

Optimistic UI Updates

Optimistic UI is an excellent model for creating websites that feel fast and responsive. However, it can be difficult to implement correctly. SWR 2.0 has added some new powerful options to make it easier.

Let’s say we have an API that adds a new todo to the todo list and sends it to the server:

await addNewTodo('New Item')

In the UI, we use a useSWR hook to display the todo list, with an “Add New Item” button that triggers this request and asks SWR to re-fetch the data via mutate():

const { mutate, data } = useSWR('/api/todos')

return <>
  <ul>{/* Display data */}</ul>

  <button onClick={async () => {
    await addNewTodo('New Item')
    mutate()
  }}>
    Add New Item
  </button>
</>

However, the await addNewTodo(...) request could be very slow. When it’s ongoing, users still see the old list even if we can already know what the new list will look like. With the new optimisticData option, we can show the new list optimistically, before the server responds:

const { mutate, data } = useSWR('/api/todos')

return <>
  <ul>{/* Display data */}</ul>

  <button onClick={() => {
    mutate(addNewTodo('New Item'), {
      optimisticData: [...data, 'New Item'],
    })
  }}>
    Add New Item
  </button>
</>

SWR will immediately update the data with the optimisticData value, and then send the request to the server. Once the request finishes, SWR will revalidate the resource to ensure it’s the latest.

Like many APIs, if the addNewTodo(...) request returns us the latest data from the server, we can directly show that result, too (instead of starting a new revalidation)! There’s the new populateCache option to tell SWR to update the local data with the mutate response:

const { mutate, data } = useSWR('/api/todos')

return <>
  <ul>{/* Display data */}</ul>

  <button onClick={() => {
    mutate(addNewTodo('New Item'), {
      optimisticData: [...data, 'New Item'],
      populateCache: true,
    })
  }}>
    Add New Item
  </button>
</>

At the same time, we don’t need another revalidation afterwards as the response data is from the source of truth, we can disable it with the revalidate option:

const { mutate, data } = useSWR('/api/todos')

return <>
  <ul>{/* Display data */}</ul>

  <button onClick={() => {
    mutate(addNewTodo('New Item'), {
      optimisticData: [...data, 'New Item'],
      populateCache: true,
      revalidate: false,
    })
  }}>
    Add New Item
  </button>
</>

Lastly, if addNewTodo(...) fails with an exception, we can revert the optimistic data ([...data, 'New Item']) we just set, by setting rollbackOnError to true (which is also the default option). When that happens, SWR will roll back data to the previous value.

const { mutate, data } = useSWR('/api/todos')

return <>
  <ul>{/* Display data */}</ul>

  <button onClick={() => {
    mutate(addNewTodo('New Item'), {
      optimisticData: [...data, 'New Item'],
      populateCache: true,
      revalidate: false,
      rollbackOnError: true,
    })
  }}>
    Add New Item
  </button>
</>

The final code will resemble the above example.

Note - The examples are not solely mine but are from the SWR 2.0 Official Docs. You can refer to the official docs at https://swr.vercel.app/docs/getting-started for more details.