Table of Contents

Lab 05 - Next.js: SSR and SSG

Single Page Applications (SPAs)

Classic web pages usually render visual elements on the screen using HTML (to structure the visual elements) and CSS (to style them), while utilizing JavaScript just to make that page interactive (e.g. trigger an action when the user clicks a button, perform error handling for forms, display additional information on hover, etc.).

In contrast, Single Page Applications or SPAs rely heavily on JavaScript for almost everything, including rendering the entire user interface (UI). That is also the case for almost every web development framework (also simply called JavaScript frameworks) out there, including Angular, Vue, and React. React is a telling example, as it uses JavaScript for writing every part of the component, including the template (JSX) and the styling (more on that in future tutorials). When you build a React application to make it ready for production, the JSX template, the styling of the components, and the JS logic are transformed using a compiler (usually Babel) and bundled together into pure JS code and optimized using a dedicated build tool (usually Webpack). The resulting JS resources together with minimal HTML and CSS boilerplate are then served to the browser when the latter performs a GET request.

Single Page Applications

An advantage of this approach is that the server's response to a client request comprises minimal HTML and CSS code and some JavaScript resources that are usually obfuscated and heavily minified. That leads to faster response times from the server and a pleasing user experience (UX), as the visual elements start to be rendered dynamically as soon as the browser starts executing the JavaScript received from the server. Moreover, when the user navigates through the app, a few requests are further sent to the server as the majority of new visual elements are displayed in the browser by executing even more JavaScript written in the same resources that were initially fetched when the page was initialized.

The big disadvantage however, is that crawlers don't like SPAs. That means that when Google's bots visit your page to index it and then rank it for search results, they usually don't like to wait for the loading and execution of JS code so they can understand what visual elements are in it. And even if the crawlers have become increasingly more performant and JS-tolerant, there is still a problem with semantics. JavaScript frameworks were not designed with semantics in mind. That means that the HTML elements that are dynamically generated at runtime usually don't respect the best practices (e.g. use paragraphs for text content, use headers in hierarchical order, include semantic elements, etc.). That is a big red flag for SEO and solving it just by using React is pretty difficult.

Pre-Rendering (SSR and SSG)

To solve the discussed issues, we can use pre-rendering. That is generating HTML and CSS code in advance, for each page instead of relying solely on the client-side execution of JavaScript. As a result, the amount of JS code executed by the browser is heavily reduced as it is used only for making the page fully interactive. This process is called hydration and a great tool for doing this in React is Next.js.

There are usually 2 types of pre-rending, both being supported by Next.js.

1. Server-Side Rendering (SSR)

In this case, the HTML and CSS code is generated on each request. This is an SEO-friendly approach that is hybrid in nature, as it allows the application to serve different content in a context-dependent fashion while minimizing the amount of JavaSript code that gets executed on the client. Hence, SSR is perfect for scenarios where real-time interactivity (e.g. a live map, live stats display, counter, etc.) is not required, but some parts of the page cannot be determined at build time, but on each HTTP request.

2. Static Site Generation (SSG)

SSG goes one step further and pre-renders the styled HTML at build time. That means that the same content will be returned for every subsequent HTTP request and data on that page will not change whatsoever. This approach is suitable for landing pages, articles, blog posts, or other web pages that solely rely on static content. This pre-rendering technique is highly recommended as it offers huge SEO benefits, but it can also be leveraged by Content Delivery Networks (CDNs) or other caching techniques (e.g. Cloudflare).

SSG Implementation

When generating content at build time, Next.js also supports fetching external data beforehand. That can be performed by defining the getStaticProps inside the file where a page is defined (more on pages later).

export default function Home(props) {
  return <p>Little known fact: {props.fact}</p>;
}
 
export async function getStaticProps() {
  const fact = getAwesomeFactFromApi();
 
  // These are the same props that are passed as an argument to the component
  return {
    props: { fact }
  };
}

The getStaticProps is executed on the server side and it is not included in the client bundle.

SSR Implementation

Likewise, external data can be fetched at request time, before bundling the web page and returning it to the client. To do that, the getServerSideProps function must be defined in the same file where the page is defined (again, more on pages later).

export default function UserProfile({ firstName, lastName }) {
  return <p>Hi! My name is {firstName} {lastName}.</p>;
}
 
export async function getServerSideProps() {
  const userId = ... // get the userId somehow (e.g. from the query param)
 
  const profileData = fetchUserData(userId);
 
  // These are the same props that are passed as an argument to the component
  return {
    props: {
      firstName: profileData.firstName,
      lastName: profileData.lastName,
    }
  };
}

The getServerSideProps is executed on the server side as well and it is not included in the client bundle. However, the server's response time will be higher because the data must be recomputed with each request and hence, it cannot be cached by a CDN.

Client-Side Rendering

Conveniently, if the getStaticProps and getServerSideProps are not defined, Next.js will automatically use the SSG approach. However, if further tweaks must be performed exclusively on the client side after the static page is received, that can be done using hooks as one normally does in React.

Next.js recommends fetching data on the client using SWR, a popular React hook just for that.

import useSWR from 'swr';
 
function Dashboard() {
  // "fetcher" can be Axios
  const { dashboardData, error } = useSWR('/api/dashboard/stats', fetcher);
 
  if (error) return <p>An error occurred!</p>;
  if (!dashboardData) return <div>Loading dashboard data...</div>;
  return (
    <div>
      <h1>My Dashboard</h1>
      <p>Visitors: {dashboardData.visitors}</p>
      <p>CPU usage: {dashboardData.cpu}%</p>
      <p>Memory usage: {dashboardData.memoryMB}MB</p>
    </div>
  );
}

Next.js Routing

Next.js has an opinionated way of handling the application routes. Each .js, .jsx, .ts, or .tsx file that sits in the pages directory represents a page and from a React perspective, it is just another component. However, these are the only files in which the getServerSideProps and getStaticProps functions can be written to inform Next.js whether those pages are using SSR or SSG.

By default, each directory represents a part of the resulting URL of your page. That also applies to those files whose names are not index. Hence, the following structures are equivalent:

  * pages/index.jsx
  * pages/blog/index.jsx
  * pages/blog/first-post/index.jsx
  * pages/index.jsx
  * pages/blog/index.jsx
  * pages/blog/first-post.jsx

And they both generate the URLs:

  * /
  * /blog
  * /blog/first-post

Page components can still make use of other components written in the components directory.

Dynamic Routes

Next.js also supports dynamic routes which are useful when the route depends on a URI parameter. Here are some examples of URLs where this might come in handy:

  * /blog/:post-name
  * /profile/:user-id
  * /videos/:video-id/statistics
  * /questions/*

In these cases, square brackets are used in the naming of files or directories to indicate that the route is dynamic:

  * /blog/[post-name].jsx
  * /profile/:[user-id].jsx
  * /videos/[video-id]/statistics.jsx
  * /questions/[...all].jsx

You can find a compelling example of how dynamic route structuring works here.

Page Linking

To link pages together, Next.js provides you with a dedicated component called Link, which works similarly to the classic anchor tag.

import Link from 'next/link';
 
function Home({ otherBlogPosts }) {
  return (
    <ul>
      <li>
        <Link href="/">Home</Link>
      </li>
      <li>
        <Link href="/blog">Thoughts</Link>
      </li>
      <li>
        <Link href="/blog/first-post">How the story beginned</Link>
      </li>
      {otherBlogPosts.map((post) => (
        <li key={post.id}>
          <Link href={`/blog/${encodeURIComponent(post.path)}`}>
            {post.name}
          </Link>
        </li>
      ))}
    </ul>
  )
}
 
export default Home;

As an alternative, if you want to navigate between pages dynamically, from your JavaScript logic, you can do that with useRouter.

import { useRouter } from 'next/router';
 
function Blog({ completePostpath }) {
  const router = useRouter();
  const handleClick = () => router.push(completePostpath); // e.g. /blog/first-post
 
  return (
    <div>
      <p>Welcome!</p>
      <button onClick={handleClick}>My first blog post</button>
    </div>
  )
}
 
export default Blog;

Any page contained by a Link will be prefetched by default (including the corresponding data) if SSG is used. The corresponding data for SSR pages is fetched only when the Link is clicked.

Disable SSR/SSG

Next.js also allows you to disable the default behavior of pre-generating the HTML and CSS code at build time by explicitly indicating that you want to dynamically load a certain component on the client side. The rendering of that component will be performed by executing JavaScript code in the browser, just like it happens with a classic SPA. This approach might be useful when you need to access client-side specific libraries (e.g. window).

import dynamic from 'next/dynamic';
 
const DynamicHeader = dynamic(() => import('../components/header'), {
  ssr: false,
});
 
function Home() {
  return (
    <div>
      <DynamicHeader/>
      <h1>Welcome to my page!</h1>
    </div>
  )
}
 
export default Home;

Layouts

If you want to reuse components on multiple pages, layouts come in handy. For instance, your app might have a header and a footer on each page:

// layouts/layout.jsx
 
import Navbar from '..components/navbar';
import Footer from '..components/footer';
 
export default function Layout({ children }) {
  return (
    <>
      <Navbar />
      <main>{children}</main>
      <Footer />
    </>
  );
}
// pages/_app.jsx
 
import Layout from '../layouts/layout';
 
export default function Home({ Component, pageProps }) {
  return (
    <Layout>
      <Component {...pageProps} />
    </Layout>
  );
}

Multiple layouts can also be used on a per-page basis. You can find more about that here.

It is good practice to place all the layouts in a dedicated layouts directory, like in the example above.

Next.js Configuration

There is a plethora of configurations supported by Next.js that can be written in the next.config.js file which resides at the root of your project. One can set environment variables, base paths for routes, internationalization, CDN support, path reroutes, etc. You can find more about that here.

Tasks

Material UI is a popular component library for React. All the necessary dependencies have already been included in the project so you can make use of the readily available components. You can check out the official docs to understand how can you include those components in your code.

For this project, we will use the Cat Facts API. The GET requests will be performed using Axios.

  1. Download the project archive for this lab and run npm install and npm run dev.
  2. Link the home page and the /tasks page together (the tasks page should be reachable from the home page and vice-versa).
  3. Create an SSR page at /cats that displays 3 random cat facts. Use a dedicated Fact component.
  4. Create an SSG page at /cats/breeds that displays the first 10 breeds in alphabetic order. Use a dedicated Breed component.
  5. Create a layout for your entire application that adds a header containing a cat logo for all of your pages (you already have the logo in the public directory).
  6. Link the homepage with the /cats page using the Link component and the /cats/breeds page using the useRouter hook.
  7. Create a page at /dynamic-tasks that is identical to the /tasks page, but it loads the TasksList component dynamically, on the client side.
  8. Build your application using npm run build and then start your production-ready application using npm run start.
  9. Open the /tasks and /dynamic-tasks pages on 2 separate tabs and inspect the page sources for both of them. Discuss your findings with the assistant.
  10. Create a custom 404 page (see this guide).

If you want to create a proper header for your application, this might come in handy.

Feedback

Please take a minute to fill in the feedback form for this lab.