Prefetching Data in a Next.js Application with SWR

Next.js comes with an amazing performance optimization where it will do code splitting of each page but if your page link to another one it will prefetch the JavaScript bundle as low priority, this way once the user navigates to another page it will, probably, already have the bundle of the new page and render it immediately, if the page is not using getInitialProps.

This works amazingly great and makes navigation super-fast, except you don't get any data prefetching benefit, your new page will render the loading state and then render with data once the requests to the API resolved successfully.

But the key thing here is that we, as a developer, may probably know which data the user will need on each page, or at least most of it, so it's possible to fetch it before the user navigates to another page.

SWR it's another great library, from the same team doing Next.js, which let use do remote data-fetching way easier, one of the best part of it is that while each call of SWR will have its own copy of the data it also has an external cache, if a new call of SWR happens it will first check in the cache to get the data and then revalidate against the API, to be sure we always have the correct data.

This cache's also updatable from the outside using a simple function called mutate which SWR gives us. This is great since we could call this function and then once a React component is rendered using SWR it will already have the data in the cache.

Running Demo

This is the final project running in CodeSandbox

<iframe src="https://codesandbox.io/embed/prefetching-data-in-a-nextjs-application-with-swr-0mvv7?fontsize=14&hidenavigation=1&theme=light&view=preview" style="width:100%;height:500px;border:0;border-radius:4px;overflow:hidden;" title="Prefetching Data in a Next.js Application with SWR" allow="geolocation; microphone; camera; midi; vr; accelerometer; gyroscope; payment; ambient-light-sensor; encrypted-media; usb" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"

</iframe>

Defining the Project

Let's say our application will have a navigation bar, this is super common, imagine we have three links.

  • Home
  • My Profile
  • Users

The Home page will show some static data, My Profile will render the current user profile page and Users will render the list of users.

So we could add this navigation bar in our pages/_app.js to ensure it's rendered in every page and it's not rerendered between navigation so we could keep states there if we needed it (we will not in our example), so let's imagine this implemented.

export default function MyApp({ Component, pageProps }) {
  return (
    <Layout>
      <Navigation>
        <NavItem label="Home" href="/" />
        <NavItem label="My Profile" href="/my-profile" />
        <NavItem label="Users" href="/users" />
      </Navigation>
      <Main>
        <Component {...pageProps} />
      </Main>
    </Layout>
  );
}

It could be something like that, Layout render a div with a CSS Grid to position the Navigation and the Main components in the correct places.

Now if the user clicks on Home we now will not show any dynamic data, so we don't care about that link, we could let Next.js prefetch the JS bundle and call it a day.

But My Profile and Users will need dynamic data from the API.

export default function MyProfile() {
  const currentUser = useCurrentUser();
  return <h2>{currentUser.displayName}</h2>;
}

That could be the MyProfile page, we call a useCurrentUser hook which will call useSWR internally to get the currently logged-in user.

export default function Users() {
  const users = useUsers();
  return (
    <section>
      <header>
        <h2>Users</h2>
      </header>
      {users.map(user => (
        <article key={user.id}>
          <h3>{user.displayName}</h3>
        </article>
      ))}
    </section>
  );
}

As in MyProfile the custom hook useUsers will call useSWR internally to get the list of users.

Applying the Optimization

Now let's define our NavItem component, right now based on our usage it may work something like this.

export default function NavItem({ href, label }) {
  return (
    <Link href={href}>
      <a>{label}</a>
    </Link>
  );
}

Let's add the prefetch, imagine we could pass a prepare function to NavItem where we could call functions to fetch the data and mutate the SWR cache.

<Navigation>
  <NavItem label="Home" href="/" />
  <NavItem
    label="My Profile"
    href="/my-profile"
    prepare={() => getCurrentUser()}
  />
  <NavItem label="Users" href="/users" prepare={() => getUsers()} />
</Navigation>

Let's make it work updating our NavItem implementation.

function noop() {} // a function that does nothing in case we didn't pass one
export default function NavItem({ href, label, prepare = noop }) {
  return (
    <Link href={href}>
      <a onMouseEnter={() => prepare}>{label}</a>
    </Link>
  );
}

Now if the user mouse enter the link, aka the user hover the link, we will call our prepare function, we could do this because if the user hovers the link it may want to click it, so we trigger the fetch of the data, once the user clicks it may have already fetched it and updated SWR cache if the user never clicks we only prefetched data and cache it for nothing but didn't lose anything.

Now let's implement getUsers and getCurrentUser functions.

export function fetcher(path) {
  return fetch(path).then(res => res.json());
}

export function fetchAndCache(key) {
  const request = fetcher(key);
  mutate(key, request, false);
  return request;
}

export function getCurrentUser() {
  return fetchAndCache("/api/users/current");
}

export function getUsers() {
  return fetchAndCache("/api/users");
}

The fetcher function triggers the fetch and parses the response as JSON.

The fetchAndCache function will call fetcher, keep the promise, not the result since we are not awaiting it or calling .then, and pass the key, our URL, to mutate along with the request promise, the false as the third argument will tell SWR to not revalidate the data against the backend, we don't need it because we just fetched it so we will it to don't do that.

Lastly getCurrentUser and getUsers are wrappers around fetchAndCache to specify a certain key (URL).

Note: We call the URL key because SWR uses the first argument as cache key too, it doesn't necessarily need to be a valid URL, e.g. we could users users or users/current and prepend /api/ in the fetcher.

With all of this once we hover over My Profile and Users it will trigger the fetch now, if we navigate to it we will see the data rendered right away without waiting, SWR will still fetch it again to revalidate once useSWR is called to be sure we always have the correct data.

Final Words

As you could see adding a simple function call before the user starts a page navigation it could help us increase the perceived performance of our application, we could continue improving this adding checks to ensure we are not prefetching data if the users are on a low-speed connection or using mobile data which could help him save data and only load what it really needs.