Chapter 2: Exploring Different Rendering Strategies

When talking about rendering strategies, we refer to how we serve a web page (or a web application) to a web browser. There are frameworks, such as Gatsby (as seen in the previous chapter), that are incredibly good at serving statically generated pages. Other frameworks will make it easy to create server-side rendered pages.

But Next.js brings those concepts to a whole new level, letting you decide which page should be rendered at build time and which should be served dynamically at runtime, regenerating the entire page for each request making certain parts of your applications incredibly dynamic. The framework also allows you to decide which components should exclusively be rendered on the client side, making your development experience extremely satisfying.

In this chapter, we'll have a closer look at:

  • How to dynamically render a page for each request using server-side rendering
  • Different ways to render certain components on the client side only
  • Generating static pages at build time
  • How to regenerate static pages in production using incremental static regeneration

Technical requirements

To run the code examples in this chapter, make sure you have Node.js and npm installed on your machine. As an alternative, you can use an online IDE such as https://repl.it or https://codesandbox.io.

You can find the code for this chapter in the GitHub repository: https://github.com/PacktPublishing/Real-World-Next.js.

Server-side rendering (SSR)

Even though server-side rendering (SSR) sounds like a new term in the developer's vocabulary, it is actually the most common way for serving web pages. If you think of languages such as PHP, Ruby, or Python, they all render the HTML on the server before sending it to the browser, which will make the markup dynamic once all the JavaScript contents have been loaded.

Well, Next.js does the same thing by dynamically rendering an HTML page on the server for each request, then sending it to the web browser. The framework will also inject its own scripts to make the server-side rendered pages dynamic in a process called hydration.

Imagine you're building a blog and you want to display all the articles written by a specific author on a single page. This can be a great use case for SSR: a user wants to access this page, so the server renders it and sends the resulting HTML to the client. At this point, the browser will download all the scripts requested by the page and hydrate the DOM, making it interactive without any kind of page refresh or glitch (you can read more about React hydration at https://reactjs.org/docs/react-dom.html#hydrate). From this point, thanks to React hydration, the web app can also become a single-page application (SPA), taking all the advantages of both client-side rendering (CSR) (as we'll see in the next section) and SSR.

Talking about the advantages of adopting a specific rendering strategy, SSR provides multiple benefits over the standard React CSR:

  • More secure web apps: Rendering a page on the server side means that activities such as managing cookies, calling private APIs, and data validation happen on the server, so we will never expose private data to the client.
  • More compatible websites: The website will be available even if the user has disabled JavaScript or uses an older browser.
  • Enhanced search engine optimization: Since the client will receive the HTML content as soon as the server renders and sends it, the search engine spiders (bots that crawl the web pages) will not need to wait for the page to be rendered on the client side. This will improve your web app's SEO score.

Despite those great advantages, there are times where SSR might not be the best solution for your website. In fact, with SSR, you will need to deploy your web application to a server that will re-render a page as soon as it's required. As we'll see later, with both CSR and static site generation (SSG), you can deploy static HTML files to any cloud provider, such as Vercel or Netlify, for free (or at a meager cost); if you're already deploying your web app using a custom server, you have to remember that an SSR app will always lead to a more significant server workload and maintenance costs.

Another thing to keep in mind when you want to server-side render your pages is that you're adding some latency to each request; your pages might need to call some external API or data source, and they'll call it for every page render. Navigating between server-side rendered pages will always be a bit slower than navigating between client-side rendered or statically served pages.

Of course, Next.js provides some great features for improving navigation performances, as we'll see in Chapter 3, Next.js Basics and Built-In Components.

Another thing to consider is that by default, a Next.js page is statically generated at build time. If we want to make it more dynamic by calling an external API, a database, or other data sources, we will need to export a particular function from our page:

function IndexPage() {

  return <div>This is the index page.</div>;

}

export default IndexPage;

As you can see, the page only prints the This is the index page. text inside a div. It doesn't need to call external APIs or any other data source to work, and its content will always be the same for each request. But now, let's pretend that we want to greet the user on every request; we will need to call a REST API on the server to get some specific user information and pass the result to the client using the Next.js flow. We will do that by using the reserved getServerSideProps function:

export async function getServerSideProps() {

  const userRequest =     await fetch('https://example.com/api/user');

  const userData = await userRequest.json();

  return {

    props: {

      user: userData

    }

  };

}

function IndexPage(props) {

  return <div>Welcome, {props.user.name}!</div>;

}

export default IndexPage;

In the preceding example, we used the Next.js reserved getServerSideProps function for making a REST API call on the server side for each request. Let's break it down into small steps so that we can better understand what we're doing:

  1. We start by exporting an async function called getServerSideProps. During the build phase, Next.js will look for every page exporting this function and make them dynamically server-side rendered for each request. All the code written within this function scope will always be executed on the server side.
  2. Inside the getServerSideProps function, we return an object containing a property called props. This is required because Next.js will inject those props inside our page component, making them available both on the client and server side. In case you're wondering, we don't need to polyfill the fetch API when we use it on the server side, as Next.js already does that for us.
  3. We then refactor the IndexPage function, which now accepts a props parameter containing all the props passed from the getServerSideProps function.

And that's all! After we ship this code, Next.js will always dynamically render our IndexPage on the server, calling an external API and showing different results as soon as we make changes in our data source.

As seen at the beginning of this section, SSR provides some significant advantages but has some caveats. If you want to use any component that relies on browser-specific APIs, you will need to render it on the browser explicitly because, by default, Next.js renders the entire page content on the server, which does not expose certain APIs, such as window or document. So here comes the concept of CSR.

Client-side rendering (CSR)

As seen in the previous chapter, a standard React app is rendered once the JavaScript bundle has been transferred from the server to the client.

If you're familiar with create-react-app (CRA), you may have noticed that right before the web app renders, the whole web page is entirely white. That's because the server only serves a very basic HTML markup, which contains all the required scripts and styles to make our web app dynamic. Let's take a closer look at that HTML generated by CRA:

<!DOCTYPE html>

<html lang="en">

  <head>

    <meta charset="utf-8" />

    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />

    <meta

      name="viewport"

      content="width=device-width, initial-scale=1"

    />

    <meta name="theme-color" content="#000000" />

    <meta

      name="description"

      content="Web site created using create-react-app"

    />

    <link rel="apple-touch-icon"

      href="%PUBLIC_URL%/logo192.png" />

    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />

    <title>React App</title>

  </head>

  <body>

    <noscript>

      You need to enable JavaScript to run this app.

    </noscript>

    <div id="root"></div>

  </body>

</html>

As you can see, we can only find one div inside the body tag: <div id="root"></div>.

During the build phase, create-react-app will inject the compiled JavaScript and CSS files into this HTML page and use the root div as a target container for rendering the whole application.

That means that once we publish this page to any hosting provider (Vercel, Netlify, Google Cloud, AWS, and so on), the first time we call the desired URL, our browser will first render the preceding HTML. Then, following the script and link tags contained in the preceding markup (injected by CRA at build time), the browser will render the whole application, making it available for any sort of interaction.

The main advantages of CSR are:

  • It makes your app feel like a native app: Downloading the whole JavaScript bundle means that you already have every page of your web app downloaded in your browser. If you want to navigate to a different page, it will swap the page content instead of downloading new content from the server. You don't need to refresh the page to update its content.
  • Page transitions made easy: Client-side navigation allows us to switch from one page to another without reloading the browser window. This comes in handy when you want to show some cool transitions between pages with ease because you don't have any reload that might interrupt your animations.
  • Lazy loading and performances: With CSR, the browser will only render the minimum HTML markup required for the web app to work. If you have a modal that appears once the user clicks on a button, its HTML markup is not present on the HTML page. It will be created dynamically by React once the button click event occurs.
  • Less server-side workload: Given that the whole rendering phase is delegated to the browser, the server only needs to send a very basic HTML page to the client. You then don't need a very powerful server; indeed, there are cases where you can host your web app on serverless environments, such as AWS Lambda, Firebase, and so on.

But all those benefits come at a cost. As we've previously seen, the server only sends an empty HTML page. If the user's internet connection is slow, the downloading of JavaScript and CSS files will take some seconds to complete, leaving the user waiting with an empty screen for several moments.

This will also affect your web app SEO score; the search engine spiders will reach your page and will find it empty. Google bots, for instance, will wait for the JavaScript bundle to be transferred but will assign a low-performance score to your website because of their waiting time.

By default, Next.js renders all the React components inside a given page on the server side (as seen in the previous section) or at build time. In the first chapter, under the Moving from React to Next.js section, we saw that the Node.js runtime doesn't expose some browser-specific APIs, such as window or document, or HTML elements, such as canvas, so if you try to render any component that needs access to those APIs, the rendering process will crash.

There are many different ways to avoid those kinds of problems with Next.js, demanding the rendering of specific components to the browser.

Using the React.useEffect hook

If you're coming from a React version before 16.8.0, you may be used to the componentDidMount method of the React.Component class. With more modern versions of React, which emphasize the usage of function components, you can achieve the same results using the React.useEffect hook. It will let you perform side effects (such as data fetching and manual DOM changes) inside your function components, and it will do it after the component has been mounted. That means that with Next.js, the useEffect callback will run on the browser after React hydration, letting you perform certain actions only on the client side.

For example, let's pretend that we want to display a code snippet on a web page using the Highlight.js library, making it easy to highlight and make code more readable. We could just create a component called Highlight, which would look as follows:

import Head from 'next/head';

import hljs from 'highlight.js';

import javascript from 'highlight.js/lib/languages/javascript';

function Highlight({ code }) {

   hljs.registerLanguage('javascript', javascript);

   hljs.initHighlighting();

  return (

    <>

      <Head>

        <link rel='stylesheet' href='/highlight.css' />

      </Head>

      <pre>

        <code className='js'>{code}</code>

      </pre>

    </>

  );

}

export default Highlight;

While this piece of code would perfectly run on a client-side React app, it will crash during the rendering or build phase on Next.js because Highlight.js needs the document global variable, which does not exist in Node.js, as it's exposed by browsers only.

You can easily fix this by wrapping all the hljs calls in the useEffect hook:

import { useEffect } from 'react';

import Head from 'next/head';

import hljs from 'highlight.js';

import javascript from

'highlight.js/lib/languages/javascript';

function Highlight({ code }) {

  useEffect(() => {

    hljs.registerLanguage('javascript', javascript);

    hljs.initHighlighting();

  }, []);

  return (

    <>

      <Head>

        <link rel='stylesheet' href='/highlight.css' />

      </Head>

      <pre>

        <code className='js'>{code}</code>

      </pre>

    </>

  );

}

export default Highlight;

That way, Next.js will render the HTML markup returned by our component, inject the Highlight.js script into our page, and, once the component is mounted on the browser, it will call the library functions on the client side.

You can also use that exact approach for rendering a component exclusively on the client side by using both React.useEffect and React.useState together:

import {useEffect, useState} from 'react';

import Highlight from '../components/Highlight';

function UseEffectPage() {

  const [isClient, setIsClient] = useState(false);

  useEffect(() => {

    setIsClient(true);

  }, []);

  return (

    <div>

      {isClient &&

        (<Highlight

          code={"console.log('Hello, world!')"}

          language='js'

        />)

      }

    </div>

  );

}

export default UseEffectPage;

That way, the Highlight component will be rendered on the browser exclusively.

Using the process.browser variable

Another way of avoiding the server-side process crashing when using browser-specific APIs is to conditionally execute scripts and components depending on the process.browser global variable. Indeed, Next.js appends this incredibly useful property to Node.js' process object. It is a Boolean value set to true when the code runs on the client side and false when running on the server. Let's see how it works:

function IndexPage() {

  const side = process.browser ? 'client' : 'server';

  return <div>You're currently on the {side}-side.</div>;

}

export default IndexPage;

If you try to run the preceding example, you will notice that for a brief moment, the browser will show the following text: You're currently running on the server-side; it will be replaced by the You're currently running on the client-side text as soon as React hydration occurs.

Using dynamic component loading

As we saw in the first chapter, Next.js extends React functionalities by adding some great built-in components and utility functions. One of these is called dynamic, and it's one of the most interesting modules provided by the framework.

Remember the Highlight.js component that we built to understand how to render a component on the browser using the React.useEffect hook? Here is another way to render it using the Next.js dynamic function:

import dynamic from 'next/dynamic';

const Highlight = dynamic(

  () => import('../components/Highlight'),

  { ssr: false }

);

import styles from '../styles/Home.module.css';

function DynamicPage() {

  return (

    <div className={styles.main}>

      <Highlight

        code={"console.log('Hello, world!')"}

        language='js'

      />

    </div>

  );

}

export default DynamicPage;

With the preceding code, we're importing our Highlight component via dynamic imports, specifying that we want it to be executed on the client only thanks to the ssr: false option. That way, Next.js won't try to render that component on the server and we'll have to wait for React hydration to make it available on the browser.

CSR can be a fantastic alternative to SSR for building very dynamic web pages. If you're working on a page that doesn't need to be indexed by search engines, it could make sense to first load your application's JavaScript, and then, from the client side, fetch any necessary data from the server; this would lighten the server-side workload since this approach does not involve SSR and your application could scale better.

So, here's a question – if we need to build a dynamic page and SEO is not really important (admin pages, private profile pages, and so on), why don't we just send a static page to the client and load all the data once the page has been transferred to the browser? We'll explore this possibility in the next section.

Static site generation

So far, we've seen two different ways of rendering our web apps: on the client side and server side. Next.js gives us a third option called static site generation (SSG).

With SSG, we will be able to pre-render some specific pages (or even the whole website if necessary) at build time; that means that when we're building our web app, there might be some pages that won't change their content very often, so it makes sense for us to serve them as static assets. Next.js will render these pages during the build phase and will always serve that specific HTML that, just like SSR, will become interactive thanks to the React hydration process.

SSG brings a lot of advantages when compared to both CSR and SSR:

  • Easy to scale: Static pages are just HTML files that can be served and cached easily by any content delivery network (from now on, CDN). But even if you want to serve them using your own web server, it will result in a very low workload, given that no hard computations are needed for serving a static asset.
  • Outstanding performances: As said before, the HTML is pre-rendered at build time, so both the client and server can bypass the runtime rendering phase for each request. The web server will send the static file and the browser will just display it, as easy as that. No data fetching is required on the server side; everything we need is already pre-rendered inside the static HTML markup, and that reduces the potential latency for each request.
  • More secure requests: We don't need to send any sensitive data to the web server for rendering the page, and that makes life a bit harder for malicious users. No access to APIs, databases, or other private information is required because every piece of information needed is already part of the pre-rendered page.

SSG is probably one of the best solutions for building performant and highly scalable frontend applications. The biggest concern about this rendering technique is that once the page has been built, the content will remain the same until the next deployment.

For instance, let's pretend that we're writing a blog post and we misspell a word in the title. Using other static site generators, such as Gatsby or Jekyll, we would need to rebuild the whole website to change just a word in a blog post title because we would need to repeat the data fetching and rendering phase at build time. Remember what we said at the beginning of this section: statically generated pages are created at build time and served as static assets for each request.

While this is true for other static site generators, Next.js provides a unique approach for solving this problem: incremental static regeneration (ISR). Thanks to ISR, we can specify at the page level how long Next.js should wait before re-rendering a static page updating its content.

For instance, let's say that we want to build a page showing some dynamic content, but the data fetching phase, for some reason, takes too long to succeed. This would lead to bad performance, giving our users a terrible user experience. A combination of SSG and ISR would solve this problem by taking a hybrid approach between SSR and SSG.

Let's pretend we've built a very complex dashboard that can handle a lot of data… but the REST API request for this data is taking up to a few seconds to succeed. In that case, we are lucky because that data won't change a lot during this time, so we can cache it for up to 10 minutes (600 seconds) using SSG and ISR:

import fetch from 'isomorphic-unfetch';

import Dashboard from './components/Dashboard';

export async function getStaticProps() {

  const userReq = await fetch('/api/user');

  const userData = await userReq.json();

  const dashboardReq = await fetch('/api/dashboard');

  const dashboardData = await dashboardReq.json();

  return {

    props: {

      user: userData,

      data: dashboardData,

    },

    revalidate: 600 // time in seconds (10 minutes)

  };

}

function IndexPage(props) {

  return (

    <div>

      <Dashboard

        user={props.user}

        data={props.data}

      />

    </div>

  );

}

export default IndexPage;

We're now using a function called getStaticProps, which looks similar to the getServerSideProps one that we saw in the previous section. As you may have guessed, getStaticProps is used at build time by Next.js for getting the data and rendering the page, and it won't be called again until the next build. As said before, while this can be incredibly powerful, it comes with a cost: if we want to update the page content, we have to rebuild the entire website.

To avoid the whole website rebuild, Next.js recently introduced an option called revalidate, which can be set inside the returning object of our getStaticProps function. It indicates after how many seconds we should rebuild the page once a new request arrives.

In the preceding code, we've set our revalidate option to 600 seconds, so Next.js will behave as follows:

  1. Next.js fills the page with the results of getStaticProps at build time, statically generating the page during the build process.
  2. In the first 10 minutes, every user will access the exact same static page.
  3. After 10 minutes, if a new request occurs, Next.js will server-side render that page, re-execute the getStaticProps function, save and cache the newly rendered page as a static asset, overriding the previous one created at build time.
  4. Every new request, within the next 10 minutes, will be served with that new statically generated page.

Remember that the ISR process is lazy, so if no requests occur after 10 minutes, Next.js won't rebuild its pages.

In case you're wondering, at the moment there's no way of forcing ISR revalidation via the API; once your website has been deployed, you'll have to wait the length of the expiration time set in the revalidate option for the page to be rebuilt.

Static-site generation is a great way to create fast and secure web pages, but sometimes we might want to have more dynamic content. Thanks to Next.js, we can always decide which page should be rendered at build time (SSG) or request time (SSR). We can take the best of both approaches by using SSG + ISR, making our pages a "hybrid" between SSR and SSG, and that's a game-changer for modern web development.

Summary

In this chapter, we've seen three different rendering strategies and why Next.js brings them to a whole new level with its hybrid rendering approach. We've also seen the benefits of these strategies, when we want to use them, and how they can affect the user experience or the server workload. We will always keep an eye on these rendering methodologies during the following chapters, adding more and more examples and use cases for each of them. They are the core concepts behind the choice of using Next.js as a framework.

In the next chapter, we're going to have a closer look at some of the most useful built-in Next.js components, its routing system, and how to manage metadata dynamically for improving both SEO and user experience.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.227.102.124