Overview of NextJs with App Router - vonschappler/Ultimate-React GitHub Wiki

A Quick review

Back in the day, websites were all rendered in the server than sent to the client, but when webapplications required to be more dynamic we moved to a new era, in which the rendering of a webpage / application was moved to the client.

Although we are now living into an era, where certain apps still make use of server side rendering, with is fueld by some "full stack frameworks" such as Next.js, Remix and many others.

What is happening in fact is that nowadays, we can see a blending between both previous eras. This blending provides us, developers new tools to make profit of both techs at once.

Comparison between Client Side Rendering and Server Side Rendering:

Client-Side Rendering (CRS) Server-Side Rendering (SSR)
HTML is rendered on the client using JavaScript HTML is rendered on the server
Slower initial page loads:
  • Bigger JavaScript bundle needs to be downloaded before the app starts
  • Data is fetched after component mount
Faster initial page loads:
  • Less JavaScript needs to be downloaded and executed
  • Data is fetched before HTML is rendered
Highly interactive, because all the code (except for data) has already been loaded Less interactive, because pages might be downloaded on demand and require full page reloads
SEO can be problematic SEO-friendly

When to use CSR and SSR?

CSR SSR
Highly interactive single-page apps Content-driven apps where SEO is essential (e-comerce, blogs, news, marketing, etc)
Apps that don't need SEO:
  • Apps used internaly by a company
  • Apps hidden behind a login
Two types of SSR:
  • Static: HTML is generated at build time
  • Dynamic: HTML is generated each time the server receives a new request

Manually created SSR with React and Node.js

In order to work with a simple SSR using React and Node.js, a file containing the server code was created initially with the following code:

// server.js
const { createServer } = require('http');

const server = createServer((req, res) => {
  res.end('Hello world');
});

server.listen(8000, () => {
  console.log('Server is running on port 8000...');
});

Afterwards, we edit the file in order to add some routes the server and listen to as well as add the functionality for it to send back to the client a basic barebones HTML file:

// server.js

// other imports

const { readFileSync } = require('fs');
const { parse } = require('url');

const htmlTemplate = readFileSync(`${__dirname}/index.html`, 'utf-8');

// add server routes
const server = createServer((req, res) => {
  const pathName = parse(req.url, true).pathname;
  if (pathName === '/') {
    res.writeHead(200, {
      'Content-Type': 'text/html',
    });
    res.end(htmlTemplate);
  } else if (pathName === '/test') {
    res.end('Test');
  } else {
    res.end('The url cannot be found');
  }
});

// server code
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    Hello world!
  </body>
</html>

Our intention though, it not to send a bare bones HTML file, but a simple React Application, which is going to be rendered on the server before being sent to the client. For that we change the file server.js, adding to it some basic React jsx code, right after the imports statements (in this case the imports are made using the commonjs syntax, instead of ES6).

// server.js

// imports

// React JSX
const pizzas = [
  {
    name: 'Focaccia',
    price: 6,
  },
  {
    name: 'Pizza Margherita',
    price: 10,
  },
  {
    name: 'Pizza Spinaci',
    price: 12,
  },
  {
    name: 'Pizza Funghi',
    price: 12,
  },
  {
    name: 'Pizza Prosciutto',
    price: 15,
  },
];

function Home() {
  return (
    <div>
      <h1>🍕 Fast React Pizza Co.</h1>
      <p>This page has been rendered with React on the server 🤯</p>

      <h2>Menu</h2>
      <ul>
        {pizzas.map((pizza) => (
          <MenuItem pizza={pizza} key={pizza.name} />
        ))}
      </ul>
    </div>
  );
}

function Counter() {
  const [count, setCount] = React.useState(0);
  return (
    <div>
      <button onClick={() => setCount((c) => c + 1)}>+1</button>
      <span>{count}</span>
    </div>
  );
}

function MenuItem({ pizza }) {
  return (
    <li>
      <h4>
        {pizza.name} (${pizza.price})
      </h4>
      <Counter />
    </li>
  );
}

// rest of the code

Because we are passing JSX code right into a Node.js server, a few dependecies are required, and those can be installed with the following command:

# Babel dependencies
npm i -D @babel/core @babel/preset-env @babel/preset-react @babel/register

# React dependencies
npm i react react-dom

We also need a configuration file for babel, .babelrc, with the code below:

{
  "presets": ["@babel/preset-env", "@babel/preset-react"]
}

After installing the required dependencies, the next step was to create a new file, start.js with the following code and edit the files package.json, index.html and server.js to the snippets below:

// start.js
require('@babel/register')({ extensions: ['.js', '.jsx'] });

require('./server.js');
// server.js

// other imports

const { renderToString } = require('react-dom/server');
const React = require('react');

// constant definitions

const server = createServer((req, res) => {
  const pathName = parse(req.url, true).pathname;
  if (pathName === '/') {
    const renderedHTML = renderToString(<Home />);
    const html = htmlTemplate.replace('%%%CONTENT%%%', renderedHTML);
    res.writeHead(200, {
      'Content-Type': 'text/html',
    });
    res.end(html);
  } else if (pathName === '/test') {
    res.end('Test');
  } else {
    res.end('The url cannot be found');
  }
});

// rest of the code
<!-- index.html  -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="root">%%%CONTENT%%%</div>
  </body>
</html>

Please note that after creating those files, the final result we have is a simple static HTML file (which can be taken as one of the downsights of having SSR, because there is no way to interact with the page). This is where the Hydration concept enters, which it's fundamental will be discussed in the next topic.


Hydration

When it comes to SSR applications, we can define Hydration as the process that adds back the interactivity and event handlers that were lost when the HTML was server-side rendered back to our React Application.

So basicaly what happens in this process is that React builds the component tree on the client and compares it with the actural server-side rendered DOM. If both match each other, React adopts it, by "watering the dry HTML with a water of interactivity", creating the whole React App back on the client.

When for some reason the SSRd and the React component tree doesn't match, a hydration error can be triggered, with one of the following (common) causes:

  • Incorrect HTML elements nesting
  • Data divergence between renders
  • Using browser-only APIs
  • Incorrect use of side effects

Using React's Hydration API

The first part of implementation is to create the javascript file which will work as our client, while editing both server.js and index.html as below:

// client.js

// this is the part of the code responsible for hydration
ReactDOM.hydrateRoot(document.getElementById('root'), <Home />);

// this is a "manual" bundling of the React components ree, by copying the same components we have on server.js and definitions such as objects, functions, etc that will interact with the component
const pizzas = [
  {
    name: 'Focaccia',
    price: 6,
  },
  {
    name: 'Pizza Margherita',
    price: 10,
  },
  {
    name: 'Pizza Spinaci',
    price: 12,
  },
  {
    name: 'Pizza Funghi',
    price: 12,
  },
  {
    name: 'Pizza Prosciutto',
    price: 15,
  },
];

function Home() {
  return (
    <div>
      <h1>🍕 Fast React Pizza Co.</h1>
      <p>This page has been rendered with React on the server 🤯</p>

      <h2>Menu</h2>
      <ul>
        {pizzas.map((pizza) => (
          <MenuItem pizza={pizza} key={pizza.name} />
        ))}
      </ul>
    </div>
  );
}

// also add the other React components added to the file server.js
//server.js

// imports

//jsx definitions

const htmlTemplate = readFileSync(`${__dirname}/index.html`, 'utf-8');
const clientJS = readFileSync(`${__dirname}/client.js`, 'utf-8');

const server = createServer((req, res) => {
  const pathName = parse(req.url, true).pathname;
  if (pathName === '/') {
    const renderedHTML = renderToString(<Home />);
    const html = htmlTemplate.replace('%%%CONTENT%%%', renderedHTML);
    res.writeHead(200, {
      'Content-Type': 'text/html',
    });
    res.end(html);
  } else if (pathName === '/client.js') {
    res.writeHead(200, {
      'Content-Type': 'application/javascript',
    });
    res.end(clientJS);
  } else {
    res.end('The url cannot be found');
  }
});

// rest of the code
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="root">%%%CONTENT%%%</div>
    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
    <script
      crossorigin
      src="https://unpkg.com/react@18/umd/react.development.js"
    ></script>
    <script
      crossorigin
      src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"
    ></script>
    <script type="text/babel" src="./client.js"></script>
  </body>
</html>

And with this, we finally have a full (but simple) React Application using SSR. Not that as stated before, SSR applications will render (paint) the HTML on screen, but with no functionalities. The process of hydration is what provided the functionality for the application and so, just after the script added to the file index.html as downloaded by the client the application has interactivity capabilities.

Keep in mind that this was just an overview of how SSR is implemented. Using frameworks such as Next.js this can be implemented easly, while also improving the whole application in various ways, as we are going to see in other sections.


What is Next.js?

Next.js is a meta-framework built on top of React, because we still can use everything that React provides us (components, props, hooks, etc), that if we consider React a framework.

Notice that Next.js is an opinionated way of building React applications, because this comes with a set of conventions and best practices when it regards to routing and data fetching, for example.

This will allows us to have a better developer experience, because those standards need to be followed by everyone on the team, and on the top of that, we can also mention the fact that Next.js allows us to build complex full-stack web applications and sites while using all cutting-edge React features that need to be integrated into a framework (suspense, server components, server actions, data streaming).

Next.js key "ingredients":

  1. Dynamic and Static SSR - we can select which SSR method for each defined route
  2. File-based routing conventions - folders are defined as routes while providing some convetions for special files such as pages, layouts and loaders
  3. Data fetching and mutation directly on the server - using server components to fetch data and server actions to mutate data
  4. Provision of optmization techniques - images, fonts, SEO and preloagins are optimized by Next.js

The ways of routing in Next.js:

App router (modern approach) Pages router (legacy)
Introduced in Next.js 13.4 (2023) Introduced in Next.js v1 (2016)
Recommended for new projects Still supported and may recieve updates in the future
Implements the React full-stack architecture (server components, server actions, streaming) Overall simpler and easy to learn
Easy to implement layouts and other features Simple things like layouts are hard to implement
Easy fetching with the fetch() right inside the components Data fetching is done by using Next.js-specific APIs
Allows the use of more advanced routing
Better developer and user experience
Caching is very aggresive and confusing
The leaning curve is much steeper

Setting up a Next.js project

In order to create a Next.js application all we need to do run the command below on the terminal:

# for the latest version
npx create-next-app@latest <project_name>

# for a specific version
npx create-next-app@<version_number> <project_name>

By entering one of those commands, a set of questions about the project settings will be asked - just answer them accordingly and you are done.


Adding routes and pages into a Next.js application

As stated before, we can define routes on Next.js just by working on the folder structure of the application.

When a new application is created, inside the folder structure, we can see an /app (or /pages) folcer in which we are going to define our routes as following:

  1. Create a folder with the name of the route
  2. Inside the folder, add a file with the name page.js, by convention, with the components to be rendered on the route created, which also by convention should export a named function called Page
// file ./app/newRoute/page.js
export default function Page() {
  return <h1>New page route</h1>;
}
  1. To create nested routes, just keep adding nested folders with the nested routes names always with a page.js file inside it
// file ./app/newRoute/nested/page.js
export default function Page() {
  return <p>This is a nested route</p>;
}

And just like that, we can create routes (and nested routes) using Next.js routes


Navigating between routes

In other do create a seamless navigation between the pages / routes created, we need to look for the component responsible by handling the navigation and make use of the bult-in Link component provided by Next.js like displayed in the snippet below:

import Link from 'next/link';

export default function Navigation() {
  return (
    <ul>
      <li>
        <Link href='/newRoute1'>New Route 1</Link>
      </li>
      <li>
        <Link href='/newRoute2'>New Route 2</Link>
      </li>
    </ul>
  );
}

There are many options on how to add the newly created navigation to our pages, instead of adding this manually on each page, the best option is to create a reusable layout with the navigation bar on it.


Layouts in Next.js:

Working with layouts in Next.js requires a layout.js file that exports a React Component with the layout to be rendered.

It's importante to note that the the case of the main application layout, the file is always located inside the /app folder and this one needs to follow some conventions / standards:

  1. The exported function is conventionally named RootLayout
  2. The exported jsx requires that both html and body tags are exported from it
  3. To render the pages/nested pages, we need to make use of the children prop

Those guidelines lead us to a simplified example of what a RootLayout would look like:

import Navigation from 'path/to/Navigation';

export default function RootLayout({ children }) {
  return (
    <html lang='en'>
      <body>
        <Navigation />
        <main>{children}</main>
      </body>
    </html>
  );
}

Layouts can carry out some sort of metadata, which can be used to control the head tag of the HTML, so for example to change the title of the application, we could change the snippet of code, by adding the code below to it:

export const metadata = {
  title: 'Some title to the application',
};

Layouts can also be nested, meaning that if we consider creating different layout styles for different pages, we can have multiple layout.js files, all of them receiving the children prop, but only the RootLayout (the layout in the root of /app folder) with the required html and body tags.


What are React Server Components?

So, to start with this topic it's important that we remamber - or state a basic definition: in a 100% client-site application, the UI in a React application is a function that runs based on the current state. We already know that, whenver a state changes, components (parts of the UI) need to re-render if they are dependent on that mutating state. Also remember that we can "hack" this whole concept by storing fetched data as a state, also.

This basic concept is great for users because the final result is a highly interactive application as it's also great for developers because we can write some small reusable packages (the components).

Let's keep in mind that even though this approach seems good enough, it has some negative points to considering, being two of them:

  • This approach requires lots of JS code that needs to be downloaded by the client
  • This approach produces what we call client-server data waterfalls, which impacts on components rendering when different components render differente pieces of data which are related each other

A possible solution to this, would be moving back to a 100% server side application model, but even though in this situation we can easly and quickly fetch data from the server, there is no interactivity at all, which can cause a bad user experience.

Keep in mind that in this kind of approach, our UI is then a function which changes the content rendered base on the data fetched and that in this model everything is faster because the client doesn't need to download any JS code.

Here than comes a question - what if it was possible to have the best of both approachs? In other words what if it was possible to have an application with high interactivity, fast data fetching and that is "not dependent" of JS files?

This is where React Server Components come - because the components create the UI as a function that depends both on data (as we have in the 100% server-side approach) an on state (as we have in the 100% client-side approach), which creates an easy way to mobe back and fourth the two aproachs easly, without degrading the application, provinding that a real world application needs.

Lets then sumarize what are React Server components:

  • It's a new full-stack architecture for React applications

  • It introduces the server as an integral part of React component tree with the addition of a new component type/group: the server components, which used to fetch data while still on the server

    Keep in mind that server components for being rendered on the server, do not have interactiviy and so, they require NO JS to "do their job", while allowing is to build the whole back-end of our application with React

With this definition, we now have tow big groups of components on our React Component tree / application:

  1. Client components, responsible by the interativity and state managment - always defined by the directive use client at the top of the component definition
  2. Server components, responsible by data fetching and data managment - the default components in pass that use the RSC architecture in full-stack frameworks such as Next.js

Comparing Client Components with Server Components

Client Components ServerComponents
Definition Opt-in using use client Default
Can use stat/hooks? Yes No
Lift state up Yes No
Props Yes Yes (as long as serializable when passed to client components)
Data fetching Also possible, preferably with 3rd party library Preferred, because we can use async / await right in the top level of the component
What it can import? Only import client components Client and server components
What it can render? Client components and server components, as long as passed as props Client and server components
When they rerender On state change On URL change

The Pros and Cons of React Server Components:

Pros:

  • It's possible to compose a full-stack application with React compontents alone
  • A single code base which cointains both front and back-end
  • Server components have a more direct and secure access to the data source
  • Eliminates the client-server waterfalls by fetching all data needed for a page at one before sending it to the client
  • Makes it possible to use huge libraries on server components "for free", those do not bundle JS code on them

Cons:

  • Makes React more complex
  • There are many more things to learn and understand
  • Hooks can't be used on server components
  • Increases the number of decisions to be done while developing an application
  • Sometimes it'll still be necessary to build an API, in the cases in where you have a mobile app which access the same data source
  • Can only be used from within a full-stack framework (it's possible to be added into front-end stacks only, such as Vite, but it would make the whole code more complex than using a full-stack framework like Next.js)

Using server components to fetch data in a page

With this whole new architecture, it's totally possible to fetch data directly inside a component. We just need to remember that, unless we opt-in that a component is defined as a client component with the use client tag added on the top of the component code, all components created are indded server components.

That said, we can EASYLY fetch data into any component making use of the fetch API, just as follows:

export default async function Page() {
  const res = await fetch('path.to.api/endpoint');
  const data = await res.json();
  console.log(data);
  return (
    <div>
      <ul>
        {data.map((item, i) => (
          <li key={i}>{item.property}</li>
        ))}
      </ul>
    </div>
  );
}

Creating Client components

As already discussed previously, when working with RSC we have to big groups of modules: the server components (which are the default components from the application) and the client components, which are the ones responsible by the interactivity of the application, always defined by the use client declaration at the begining of the code.

It's then, now time to work with those components, so we can understand how those can be used on RSC based applications. As a starter, lets star with a button with a simple functionality.

// this is required, so Next.js can bundle the JS code necessary to allow this button to "react" to the user interaction
'use client';

import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <span>
        <button onClick={() => setCount((c) => c + 1)}>Add count</button>{' '}
        {count}
      </span>
    </div>
  );
}

We can also read data from a server component into client components (passed as props), like in the snippet of code below:

'use client';

import { useState } from 'react';

export default function ClientComponent({ serverData }) {
  const [count, setCount] = useState(serverData.lenght);
  return (
    <div>
      <p>There are {serverData.length} entries</p>
      <span>
        <button onClick={() => setCount((c) => c + 1)}>{count}</button>
      </span>
    </div>
  );
}

and importing this client component inside a server component with the data passed as a prop to it, like so:

import Counter from 'path/to/ClientComponent';

export default async function Page() {
  const res = await fetch('path.to.api/endpoint');
  const data = await res.json();
  return (
    <div>
      <Counter serverData={data} />
    </div>
  );
}

There aer a few points that we need to consider:

  1. Both server and client components are rendered on the server, so client components which receive any server component data as a prop will have this data (or what computation done with it) displayed as soon as the served HTML is rendered on the screen
  2. As already stated, any funcionality required on client components is sent to the client on demand, delaying any interaction with the application until the bundled JS required by the client component is fully downloaded by the client
  3. To improve user experience in a way to inform the client that the page is not yet fully loaded of that some data is still being fetched, it would be part of the best practices to improve a loader component, which is the next topic of this section

Creating a loader indicator

Because Next.js is an opiniated framework, it has a set of convetions, as already discussed in situations for files with some specific names, such as page.js and layout.js. Another of this convetions is the loading.js file, which can be used it inform the client that some data is still being fetched from the server by the server compontents.

Following these conventions, the loading.js file should be placed in the root of the /app folder if we want to use this globally in our application.


A deep look into RSC

Lets imagine an application with multiple instances of server components and client components for this overview (following the new RSC architecture).

When an application like this needs to be rendered, what happens on the first stage of rendering is that all server components are rendered on the server, returning React Elements, with no code in them - this is called code disapearing, while preserving a sort of "placeholder" in where the client components will be rendered in a later stage of this process.

This needs to be that way, because all the code that needs to be sent to the client needs to be serialized which means that no functions and classes, for example, can be present in these elements, becuase per nature, those are not serializable.

Keep in mind that the server do not track any sort of fiber tree on this stage.

The placeholders mentioned above store two important referenced informations:

  1. The serialized props which will be passed from server components to client components
  2. The URL to the script with of the component code, powered by the framework's bundler

This whole stage creates something that we call RSC payload which can bem simplified as a "Virtual DOM of rendered server components with the addition of subtrees of unrendered client components".

This RSC is what's sent to the client as a Json data structure, which is a data format created by React team, in a way to enable streaming of this data.

When this structure is sent to the client, finally we have the rendering of the client components, which also return their own React components, providing then the "complete Virtual DOM" which we are already used in React applications.

The big question is: why do we need RSC payloads?

  1. React "likes" to describe UI as data instead of finished HTML, recause it gives React the ability to "react" over server components rerenders
  2. When a server component is re-rendered, React is able to reconcile the current tree on the client with a new tree, coming from the server
  3. This way state can be preserved when a server component is re-rendered - which would not happen if this process worked in a different way, forcing then full page reloads and and resulting into a loss of state and an awful user experience

The relationship between RSC and SSR:

In order to be able to create this relationship, we fist need to note that RSC and SSR are not the same - they are different technologies.

In fact RSC does not replace SSR, those technologies work together in combination by a framework.

This means that both client and server components are initially rendered on the server, when SSR is used.

NOTES

  1. Do not confuse web server (SSR) with server (React Server) in RSC. They are different things, maning that in RSC model, the server just means the "developer's computer" or a different computer from the web client, which does not need to be the actual web server.
  2. As a result, RSC does NOT require a running web server, since all components can all be only once at built time (which as called static site generation).

Now, joining all the pieces we have discussed so far:

  • The web server renders all React components and the SSR hapeens only during the initial render (subsequent re-renders happens on the web client)
  • On the inital render, the HTML that needs to be provided to the web client is generated on the web server, alongside with the RSC payload and the JS code required by the client components (split in chunks which are streamed as necessary) when a web client requests it
  • The process of hydration in this scenario happens only on client components, because as we already know, server clients have no JS code attached to it
  • The final result is a fully interactive React application
  • When ever a server component is re-rendered, the web server generate a new RSC payload, which is then sent to the web client to be reconcile with the already existing UI existent on the user's computer, preserving state - as we discussed before
⚠️ **GitHub.com Fallback** ⚠️