Next.js isomorphic fetch in getInitialProps
I've enjoyed working with Next.js for a little while now, part of a project Jon&Jess is building for a client.
The project has grown fairly in the past few months, including a custom Express backend, authentication and resource creation/editing and fetching.
Just like any other software project, we've faced our fair share of bugs, issues and optimisations.
One of the issues we've faced, which was very weird at first but made total sense once we understood what was happening, is how to handle getInitialProps
, with async and isomorphic (both server and client side) resource fetching.
Context - What we are doing
If you are unfamiliar with Next.js, the way it works is similar to "functional core, imperative shell" - or at least it makes it very easy and nice to follow this pattern.
At a high level, a pages
folder contains your "imperative shell" - your main, container components that will handle state, actions and callbacks.
Those container components only render stateless, layout components located in your src
folder - the functional core.
Pages component then, are where your data is being loaded, both when the component is being rendered on the server during an initial request, or on the client following a page transition. This is done through the component's getInitialProps
function - it's a Next.js thing. Whatever object is returned by this function is injected as props to your component.
So let's say that you have a page component called Posts
which, you guessed it, will fetch a list of blog posts and show them on the page. A very simple implementation would look as follow:
import {Component} from 'react'
import API from '../src/lib/api'
import PostList from '../src/postlist'
class Posts extends Component {
state = {
posts: this.props.posts
}
async static getInitialProps() {
const posts = await API.getPosts()
return {posts}
}
render() {
return <PostList posts={this.props.posts} />
}
}
Easy enough, right? We have an API module that allows us to fetch our posts, it's an async function so we can use await to synchronously fetch the posts and only render the page once we have the posts.
This all works, both on the server and the client.
Problem - limitations we are facing here
Looking at this component while being rendered on the server, we see no issues. All the data needs to be loaded synchronously before a response is sent to the browser, or else you'd have an empty PostList
sent to the browser before the posts have been returned.
Can you spot the issue with the client-side rendering?
getInitialProps
is a static, blocking function. Static functions in ES6 are functions called on the class directly, not on an instance of the class. And it's blocking, meaning that the component won't be initialised/rendered until that function returns.
Thinking about it, this makes total sense, right? The goal of this function is to return the props to be injected in the component - so Next.js has to wait for its return to know what to inject.
That also means that on the client side, nothing will happen until the function has returned its props. The result of that is a delay in the page transition. Say you are on the home page of our website, and click on a "posts" link that will bring you to our Posts
page. Nothing will happen until Posts.getInitialProps
returns its posts. Only then, the browser's window will start the page transition and painting the first render.
This might seem like a minor issue. It is, until it's not anymore.
We ourselves didn't notice this issue for a while. The page transitions felt a bit slow, but we didn't think much about it. Might have been Next that is slow. Or Javascript bloat.
But then, we decided to change our generic loader to use content loaders (SO to create-content-loaders!). As you can see, those content loaders are supposed to look like the page you are loading, so going from the homepage to our posts, we should see a loader looking similar to the list of posts we'd show.
Except we didn't. We'd be seeing the current page's loader. Why? Well, the flow would go as follows:
- switch "loading" state to true (we use redux for state)
- start fetching posts
- receive posts
- switch "loading" state to false
So what happens? Well, the Posts
render doesn't happen until posts are received, so when loading
is set to true, whatever current component re-renders into a loading state. In that case, that would be our home page. The real flow was as follow:
- "loading" to true. Homepage re-renders with loading state
- Posts.getInitialProps start fetching posts
- Posts.getInitialProps receives posts, switch loading to false, returns posts
- Posts is rendered.
Trust me, that was not straight forward to debug!
Solution - spoiler alert: isomorphic is great, but not so real
We now have to think differently on how to fetch our resources depending on wether we are server or client side.
The idea of isomorphism (aka code that runs/renders both on server and client side) is great, but the challenges coming with it are often understated. Next.js is a great proof that isomorphism provides awesome results. But even our simple example is also proof that isomorphism is about a result, not a way.
The result will be to have our components render both on server and client side.
The way to achieve this will be different on server and on client. Extremely rarely you'll be able to render your components on both sides using the exact same patterns.
Ok, let's get back to our example.
To have our posts fetched and rendered on our server, we have no choice but fetching them synchronously, else the HTML sent to the browser would not have the posts.
Technically we can fetch our posts synchronously on our client, but that's not a great UX and not how you'd be fetching your posts if you were doing it only client side.
So first, let's isolate our synchronous fetching to be called only on server side.
Next.js calls getInitialProps
with a context object including the HTTP request
object when server side. We can then look for this object to check if we are server side or not.
static async getInitialProps({req}) {
if (req) {
const posts = await API.getPosts()
return {posts}
} else {
API.getPosts().then( posts => this.setState({posts}) )
return {}
}
}
This would do. We check if req
is present, in which case we synchronously call our api and return our posts.
If we're on the client, notice the use of then
which doesn't block code execution like await
does. We still need to return an empty object.
Of course that implies that our Posts
page and our PostsList
component need to be able to handle being rendered without posts. For example we can assume that either Posts
or PostsList
would render a loading screen when posts
is undefined.
Conclusion: isomorphic means possible both sides, not same both sides
As I've stated earlier in the post, isomorphism is most about being being able to render your javascript both on server and client, less about doing so the same way.
It is important to be clear about the difference, because like everything else, it is more complex than what is implied in the sales pitch.
Next.js is a great framework by the way, and it makes isomorphic apps so-much-easier. It is simple, easy to learn and flexible to extend to your needs. I'd strongly recommend looking into it for your next React project.