Using vercel’s swr with next.js server side rendering
I love vercel’s swr package, it solves a many
problems at once, in an elegant way. The only thing that disappointed me,
especially considering it’s coming straight from next.js’ creators, is the lack
of integration with next.js for universal rendering. With apollo for example,
you can use getDataFromTree
to block the rendering on the server until all
queries are fetched. This would have been nice for swr
too, instead we have
to content ourselves with the possibility of adding initial data into the hook.
This means that we have to build the fetching logic twice - once on the server
with getInitialProps
or getServerProps
and once on the client with
useSWR
. It also means that we can’t abstract everything into hooks, because
that’s not supported inside getInitialProps
.
Fair enough, we’re getting all this stuff for free anyway, so let’s see what damage we can do with this. The example from the swr docs only mentions static generation, however for one specific use case I needed to do server rendering.
This construct is what I came up with:
import useSWR from 'swr'
const fetcher = (...args) =>
fetch(...args).then(res => res.json())
const Posts = props => {
const { data, loading } =
useSWR('/api/posts', fetcher, {fallbackData: props.posts})
...
}
Posts.getInitialProps = async () => {
if (typeof window !== 'undefined') {
return {}
}
return await fetcher('/api/posts')
}
Pretty similar to vercel’s example, except for a few important differences:
- It’s necessary to use
getInitialProps
here, even though discouraged by vercel. If we usegetServerProps
, we need to wait for a round trip time to the server, needlessly. - If we are running
getInitialProps
upon client side navigation, we don’t want to do anything, sinceuseSWR
will do the fetching for us in this case - This is a reduced example, but often your fetching logic is more complex –
e.g. fetching different things based on query parameters, pagination, etc.
In this case it’s helpful to extract this logic into helper functions that
you can reuse on the server and in the client. Hooks are off the table
unfortunately, because this needs to work in
getInitialProps
This configuration enables a great experience for users: you get full server side rendering, the possibility of displaying a loader while data is being fetched on the client, and a super snappy experience because client side navigation is now instant.
Authentication and pagination
This is a very basic example though, let’s see how a more complex version would look, including API authentication and pagination with endless scrolling. First we need to extend our fetcher to send the token in a header (this is a common authentication method, but you can also use any other method here):
const fetcher = async (path, token) => {
const res = await fetch(path, {
method: "GET",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
});
return await res.json();
};
Now we want a hook to hide the complexity in, so we only have to use a nice
one-line hook to fetch our data in our app. Since we are using pagination, we
want to be using useSWRInfinite
here, which has a slightly different API: it
returns an array of API responses, and its first argument is a function that
returns the API path, receiving the page index as an argument.
// fallbackData here is the first page that you have
// fetched from getInitialProps
const usePosts = (fallbackData, token) => {
const { data, size, setSize, error } = useSWRInfinite(
(index) => [`/api/posts?page=${index}`, token],
fetcher,
{
fallbackData: [fallbackData],
},
);
return {
data,
size,
setSize,
error,
};
};
Note that we are returning [path, token]
in the function. This is what will
be passed to our fetcher as arguments, and is used by swr as a cache key. It’s
important to pass the token this way, so that the cache is invalidated for
different tokens. Plainly said, if you log out and the token is empty, you
don’t want to return the value from the cache that you got with the token –
you’d want an empty response or an error.
You can now use setSize
to increase the page size, for example when a user
has scrolled to the bottom, you might want to increase the page size by one,
then swr
will automatically append the next page from the API to the data
array.
In this example, our getInitialProps
would look something like this:
Posts.getInitialProps = async ({ req }) => {
if (typeof window !== "undefined") {
return {};
}
const { token } = parseCookies(req.headers.cookie);
const posts = await fetcher("/api/posts?page=0", token);
return {
posts,
token,
};
};
For this approach to work with full SSR, you need to be storing the token in a cookie, so we can read it on the server. Admittedly, server side rendering is not a huge requirement for authenticated pages, because search engines won’t be able to read these pages anyway, but it’s still nice to return a hydrated page on the first request.
The last missing piece is how to use this whole thing in the render function:
const Posts = props => {
const { data, size, setSize } = usePosts(props.posts, props.token)
...
}
Your data
will now contain server-fetched data on the first render, and
client-fetched data on client rendered pages, without any communication with
the next.js server in between.
No AI tooling was used in the creation of this article. More articles: