Dealing with Asynchronous APIs in Server-rendered React

Share this article

Asynchronous APIs in Server-rendered React

If you’ve ever made a basic React app page, it probably suffered from poor SEO and performance issues on slower devices. You can add back traditional server-side rendering of web pages, typically with NodeJS, but this isn’t a straightforward process, especially with asynchronous APIs.

The two main benefits you get from rendering your code on the server are:

  • increased performance in load times
  • improving the flexibility of your SEO.

Remember that Google does wait for your JavaScript to load, so simple things like title content will change without issue. (I can’t speak for other search engines, though, or how reliable that is.)

In this post, I’ll discuss getting data from asynchronous APIs when using server-rendered React code. React code has the entire structure of the app built in JavaScript. This means that, unlike traditional MVC patterns with a controller, you don’t know what data you need until the app is rendered. With a framework like Create React App, you can quickly create a working app of very high quality, but it requires you to handle rendering only on the client. There’s a performance issue with this, as well as an SEO/data issue, where traditional templating engines you can alter the head as you see fit.

The Problem

React renders synchronously for the most part, so if you don’t have the data, you render a loading screen and wait for the data to come. This doesn’t work so well from the server, because you don’t know what you need until you’ve rendered, or you know what you need but you’ve already rendered.

Check out this stock-standard render method:

ReactDOM.render(
  <provider store={store}>
    <browserrouter>
      <app></app>
    </browserrouter>
  </provider>
, document.getElementById('root')
)

Issues:

  1. It’s a DOM render looking for a root element. This doesn’t exist on my server, so we have to separate that.
  2. We don’t have access to anything outside our main root element. We can’t set Facebook tags, title, description, various SEO tags, and we don’t have control over the rest of the DOM outside the element, especially the head.
  3. We’re providing some state, but the server and client have different states. We need to consider how to handle that state (in this case, Redux).

So I’ve used two libraries here, and they’re pretty popular, so hopefully it carries over to the other libraries you’re using.

Redux: Storing state where your server and client are synced is a nightmare issue. It’s very costly, and usually leads to complex bugs. On the server side, ideally, you don’t want to do anything with Redux apart from just enough to get things working and rendering correctly. (You can still use it as normal; just set enough of the state to look like the client.) If you want to try, check out the various distributed systems guides as a starting point.

React-Router: FYI, this is the v4 version, which is what is installed by default, but it’s significantly different if you’ve got an older existing project. You need to make sure you handle your routing server side and client side and with v4 — and it’s very good at this.

After all, what if you need to make a database call? Suddenly this becomes a big issue, because it’s async and it’s inside your component. Of course, this isn’t a new issue: check it out on the official React repo.

You have to render in order to determine what dependencies you need — which need to be determined at runtime — and to fetch those dependencies before serving to your client.

Existing Solutions

Below, I’ll review the solutions that are currently on offer to solve this problem.

Next.js

Before we go anywhere, if you want production, server-side-rendered React code or universal app, Next.js] is where you want to go. It works, it’s clean, and it’s got Zeit backing it.

However, it’s opinionated, you have to use their toolchain, and the way they handle async data loading isn’t necessarily that flexible.

Check out this direct copy from the Next.js repo documentation:

import React from 'react'
export default class extends React.Component {
  static async getInitialProps ({ req }) {
    return req
      ? { userAgent: req.headers['user-agent'] }
      : { userAgent: navigator.userAgent }
  }
  render () {
    return <div>
      Hello World {this.props.userAgent}
    </div>
  }
}

getInitialProps is the key there, which returns a promise that resolves to an object that populates props, and only on a page. What’s great is that’s just built in to their toolchain: add it and it works, no work required!

So how do you get database data? You make an API call. You don’t want to? Well, that’s too bad. (Okay, so you can add custom things, but you have to fully implement it yourself.) If you think about this, though, it’s a very reasonable and, generally speaking, good practice, because otherwise, your client would still be making the same API call, and latency on your server is virtually negligible.

You’re also limited in what you have access to — pretty much just the request object; and again, this seems like good practice, because you don’t have access to your state, which would be different on your server versus client anyways. Oh, and in case you didn’t catch it before, it only works on top-level page components.

Redux Connect

Redux Connect is a very opinionated server-side renderer, with a decent philosophy, but if you don’t use all the tools they describe, this might not be for you. There’s a lot to this package, but it’s so complex and not yet upgraded to React Router v4. There’s a lot of setup to this, but let’s take the most important part, just to learn some lessons:

// 1. Connect your data, similar to react-redux @connect
@asyncConnect([{
  key: 'lunch',
  promise: ({ params, helpers }) => Promise.resolve({ id: 1, name: 'Borsch' })
}])
class App extends React.Component {
  render() {
    // 2. access data as props
    const lunch = this.props.lunch
    return (
      <div>{lunch.name}</div>
    )
  }
}

Decorators aren’t standard in JavaScript. They’re Stage 2 at the time of writing, so use at your discretion. It’s just another way of adding higher-order components. The idea is pretty simple: the key is for what to pass to your props, and then you have a list of promises, which resolve and are passed in. This seems pretty good. Perhaps an alternative is simply this:

@asyncConnect([{
  lunch: ({ params, helpers }) => Promise.resolve({ id: 1, name: 'Borsch' })
}])

That seems doable with JavaScript without too many issues.

react-frontload

The react-frontload repo doesn’t have a lot of documentation, or explanation, but perhaps the best understanding I could get was from the tests (such as this one) and just reading the source code. When something is mounted, it’s added to a promise queue, and when that resolves, it’s served. What it does is pretty good, though it’s hard to recommend something that’s not well documented, maintained or used:

const App = () => (
  <frontload isServer >
    <component1 entityId='1' store={store}></component1>
  </frontload>
)

return frontloadServerRender(() => (
  render(<app></app>)
)).then((serverRenderedMarkup) => {
  console.log(serverRenderedMarkup)
})

Finding a Better Solution

None of the solutions above really resonated with the flexibility and simplicity I would expect from a library, so now I’ll present my own implementation. The goal is not to write a package, but for you to understand how to write your own package, for your use case.

The repo for this example solution is here.

Theory

The idea behind this is relatively straightforward, though it ends up being a fair bit of code. This is to give an overview of the ideas we’re discussing.

The server has to render the React code twice, and we’ll just use renderToString for that. We want to maintain a context between first and second renders. On our first render, we’re trying to get any API calls, promises and asynchronous actions out of the way. On our second render, we want to get all the data we acquired and put it back in our context, therefore rendering out our working page for distribution. This also means that the app code needs to perform actions (or not) based on the context, such as whether on the server or on the client, whether or not data is being fetched in either case.

Also, we can customize this however we want. In this case, we change the status code and head based on our context.

First Render

Inside your code, you need to know you’re working off the server or your browser, and ideally you want to have complex control over that. With React Router, you get a static context prop, which is great, so we’ll be using that. For now, we’ve just added a data object and the request data as we learned from Next.js. Our APIs are different between the server and the client, so you need to provide a server API, preferably with a similar interface as your client-side API:

const context = {data: {}, head: [], req, api}
const store = configureStore()
renderToString(
  <provider store={store}>
    <staticrouter location={req.url}
      context={context}
    >
      <app></app>
    </staticrouter>
  </provider>
)

Second Render

Right after your first render, we’ll just grab those pending promises and wait till those promises are done, then re-render, updating the context:

const keys = Object.keys(context.data)
const promises = keys.map(k=>context.data[k])
try {
  const resolved = await Promise.all(promises)
  resolved.forEach((r,i)=>context.data[keys[i]]=r)
} catch (err) {
  // Render a better page than that? or just send the original markup, let the front end handle it. Many options here
  return res.status(400).json({message: "Uhhh, some thing didn't work"})
}
const markup = renderToString(
  <provider store={store}>
    <staticrouter location={req.url}
      context={context}
    >
      <app></app>
    </staticrouter>
  </provider>
)

App

Quick jump away from our server to app code: in any of our components that have the router connection, we can now get that:

class FirstPage extends Component {
  async componentWillMount(){
    this.state = {text: 'loading'}

    this._handleData('firstPage')
  }
  async _handleData(key){
    const {staticContext} = this.props

    if (staticContext && staticContext.data[key]){
      const {text, data} = staticContext.data[key]
      this.setState({text, data})
      staticContext.head.push(
        <meta name="description" content={"Some description: "+text}/>
      )
    } else if (staticContext){
      staticContext.data[key] = this._getData()
    } else if (!staticContext && window.DATA[key]){
      const {text, data} = window.DATA[key]
      this.state = {...this.state, text, data}
      window.DATA[key] = null
    } else if (!staticContext) {
      const {text, data} = await this._getData()
      this.setState({text, data})
    }
  }
  async _getData(){
    const {staticContext} = this.props
    const myApi = staticContext ? staticContext.api : api
    const resp = await butter.post.list()
    const {data} = resp.data
    const {text} = await myApi.getMain()
    return {text, data}
  }
  render() {
    const text = this.state.text
    return (
      <div className='FirstPage'>
        {text}
      </div>
    )
  }
}

Wow, that’s a lot of complex code. At this stage, you probably want to take a more relay approach, where you separate your data fetch code in to another component.

This component is bookended by things you’re probably familiar with — a render step and a componentWillMount step. The four-stage if statement handles the different states — prefetch, post fetch, preserver render, post server render. We also add to the head after our data is loaded.

Finally, there’s a get data step. Ideally, your API and database have the same API, which makes execution the same. You’ll probably want to put these into an action in Thunk or Saga to make it more extensible.

Checkout the article “Server-Side React Rendering” and the repo React Server-side Rendering for more information. Remember, you still need to handle the state where your data isn’t loaded! You’ll only be doing a server render on first load, so you’ll be showing loading screens on subsequent pages.

Change index.html for adding data

We need to send any prefetched data as part of our page request, so we’ll add a script tag:

<script>
window.DATA = {data:{}} // It doesn't really matter what this is, just keep it valid and replaceable
</script>

Serving

Then we need to add it to our search and replace. However, HTML uses a very basic script tag finder, so you’ll need to base-64 encode it if you have script tags. Also, don’t forget about our head tags!

// earlier on
const headMarkup = context.head.map(h=>(
  renderToStaticMarkup(h)
)).join('')

// then render
const RenderedApp = htmlData.replace('{{SSR}}', markup)
  .replace('{{head}}', headMarkup)
  .replace('{data:{}}', JSON.stringify(new Buffer(JSON.stringify(context.data)).toString('base64')))
if (context.code)
  res.status(context.code)
res.send(RenderedApp)

We also handle status code changes — for example, for a 404 — so if you have a 404 page you can just do this:

class NoMatch extends Component {
  componentWillMount(){
    const {staticContext} = this.props
    if (staticContext){
      staticContext.code = 404
    }
  }
  render() {
    return (
      <div>
        Sorry, page not found
      </div>
    )
  }
}

Summary

If you’re not sure what you’re doing, just use Next.js. It’s designed for server-side rendering and universal applications, or if you want the flexibility of doing everything manually, just the way you want. An example might include if you have data fetching in sub-components rather than at the page level.

Hopefully this article has helped you on your way! Don’t forget to checkout the GitHub repo for a working implementation.

Frequently Asked Questions (FAQs) about Asynchronous APIs and Server-Rendered React

What is the difference between server-side rendering and client-side rendering in React?

Server-side rendering (SSR) and client-side rendering (CSR) are two different approaches to rendering a webpage. In SSR, the server generates the full HTML for a page in response to a request, which is then sent to the client. This results in a faster initial page load time and is beneficial for SEO. However, it can lead to slower page transitions as the entire page needs to be rendered for each request. On the other hand, CSR means that rendering happens in the browser using JavaScript. This results in slower initial page load times but faster page transitions as only the necessary components are re-rendered.

How do I make a server-side request in my client-side rendered React app?

To make a server-side request in a client-side rendered React app, you can use the fetch API or libraries like axios. You can make the request in the componentDidMount lifecycle method or inside a useEffect hook if you’re using functional components. The response can then be set to the state and used in your component.

Why is my global variable executed twice in React?

This could be due to the way React batches state updates. If you’re updating a global variable inside a React component, it might be updated twice due to the asynchronous nature of setState. To avoid this, you can use the functional form of setState, which ensures that the state update is based on the previous state, not the current one.

How can I use asynchronous APIs in server-rendered React?

To use asynchronous APIs in server-rendered React, you can use the async/await syntax in your server-side code. This allows you to wait for the API response before rendering the page. You can use libraries like axios for making the API requests.

What are the benefits of server-side rendering in React?

Server-side rendering in React has several benefits. It improves the initial page load time, which can lead to a better user experience. It also improves SEO as search engine crawlers can index the server-rendered content more easily. Additionally, it allows for a more consistent initial state, as the same code is run on both the server and the client.

How can I handle errors when using asynchronous APIs in server-rendered React?

You can handle errors by using try/catch blocks in your async functions. This allows you to catch any errors that occur when making the API request and handle them appropriately, such as by rendering an error message.

Can I use hooks in server-rendered React?

Yes, you can use hooks in server-rendered React. However, keep in mind that hooks can only be used in functional components, not class components. Also, some hooks, like useEffect, don’t run on the server, so you need to ensure that your code can handle this.

How can I improve the performance of my server-rendered React app?

There are several ways to improve the performance of a server-rendered React app. You can use code splitting to only load the necessary code for each page. You can also use caching to avoid re-rendering pages that haven’t changed. Additionally, optimizing your server-side code can help improve performance.

How can I test my server-rendered React app?

You can use testing libraries like Jest and React Testing Library to test your server-rendered React app. These libraries allow you to test your components in isolation and ensure that they render correctly.

Can I use server-side rendering with Next.js?

Yes, Next.js is a framework for React that supports server-side rendering out of the box. It provides a simple API for server-side rendering and also supports static site generation and client-side rendering.

Roger JinRoger Jin
View Author

Roger is a developer at ButterCMS.

apiasynchronousnilsonjRalphMReactreact-hubReact-Projects
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week