DEV Community

Sergio Daniel Xalambrí
Sergio Daniel Xalambrí

Posted on • Updated on • Originally published at sergiodxa.com

Prefetching Data in a Next.js Application with SWR

Originally published at https://sergiodxa.com/articles/next-swr-prefetch/

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

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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");
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (2)

Collapse
 
julioocz profile image
Julio Navarro

Cool article! It sounds great the idea of the prefetching.

I have a few concerns though:

  • What happens to the request if the user immediately clicks the the nav item and the request hasn’t resolve? Do it continues and there is some gains from it or SWR just drops the request? A lot of the times a user is going to another route it clicks it immediately.

  • It looks like going with this approach requires to no use getInitialProps which is one of the main next.js features, do you think it’s worth the tradeoff?

Collapse
 
sergiodxa profile image
Sergio Daniel Xalambrí • Edited

Good questions, for the first one it depends on how you use mutate, basically you have two ways.

  1. Call mutate(key, data)
  2. Call mutate(key, promise)

If you use the first one you will need to await for the fetch to resolve and then call mutate.

function fetchAndCache(key) {
  const data = await fetcher(key);
  mutate(key, data, false);
  return data;
} 

Something like that, that works most of the time but if the users immediately click the link to navigate to a page it will start a new request because it doesn't know you are already fetching the data somewhere else, this could cause a race condition where the second request (the one started by SWR itself) resolves faster than the first one (started by your prefetching), in that case we want only the second one to be used but the first one will overwrite the second one.

If we use the second way passing the promise what will happen is that SWR will know a request for that cache key is running, once the user navigate to the page using the same key SWR will avoid running a request immediately and wait for the promise to resolve. So no race condition here.

And if the user ends up clicking on another route what will happen is that you fetched the data but you didn't needed it, in that case the data will be cached and nothing else happens, the only downside is that you prefetched something and you are not using it yet, but it wouldn't negatively affect you.

About your second question, I'm not using getInitialProps anymore, Next.js detects the usage of it to do SSR and if you are not using it, it does SSG, basically I only let Next.js render the loading state of each page to an HTML and then load the whole data client side, as a JAMStack application, if a request fails because the user is not authenticated I'm throwing an error and using error boundaries to redirect to the login page.

I think handling data fetching this way makes easier to handle error like the unauthorized cases. Using SSG will also gives you better performance than doing SSR.