Learn Next.js Server Side Rendering by building your own implementation
Published: 2023-10-11 19:10:09

If you're here, most probably you have issues understanding how *Server Side Rendering* (SSR) works in meta frameworks like Next.js. I had them too and this is why I decided that I want to learn how they work under the hood and the best thing to do it - build it yourself.

> Of course you could also read Next.js source code, but it's really hard to understand it if you never had experience with projects so big and complex.

So, where are we at?

## Understand Server Side Rendering

First, to get a good understanding of what we are about to do, let's talk about what *Server Side Rendering* is.

![Jack Black opens up a book GIF](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3iy0xh8ct7jpe4zlnng4.gif)

**Server Side Rendering** is a process where a web server generates an HTML page on the server itself, rather than letting JavaScript handle that on the client (browser).

When a user visits a web page, the server will:

1. Get the data from the database if required
    
2. Inject this data into the HTML template
    
3. Respond with the resulting HTML back to the client
    

In the end, the browser would receive a generated HTML page and then request all the necessary static assets like CSS, JavaScript, etc.

This is a very well-known process that was around for ages, but not in the React world. Next.js (along with other meta-frameworks) has popularised a new approach, where you basically write the same React as usual with a few new ways of fetching data and underlying technology will take care of rendering React on the server and delivering a complete HTML page to the client, instead of originally bare minimum HTML and a lot of JavaScript.

> Next.js does way more than just that, but we only care about SSR here 😌

Those who are new in the Next.js world may wonder, how the heck React - a frontend JavaScript framework can be rendered on the server but still has all the features like `useState`, `useEffect`, etc on the client?

So, let's recreate Next.js *Server Side Rendering* functionality from scratch and go step by step to understand "what the heck is going on" and "how the heck `getServerSideProps` function is even called"?

Welcome to **"Learn Next.js Server Side Rendering by building your own" 😁**

## Project Setup


![Girl building with hammer](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kf296fb18cehpl44tip3.gif)

In order to create SSR functionality, first we need to create that very first **'S'** - server. Since I don't know anything else than JavaScript (TypeScript doesn't count, okay?) and also Next.js uses Node.js under the hood, **AND ALSO** we still need to work with React since it uses JavaScript - let's start with that.

### Creating Node.js Server

For this tutorial, I want to create a bare-bone Node.js application and start building it along with you, so we don't miss anything 😁

**Ready?**

#### Start New Project

Create a new folder `react-ssr`, **cd** into it and generate a new npm project by running:

```bash
npm init
```

![GIF showing the creation of react-ssr folder and npm init command](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/28ygg7kjwsq9ejov4ots.gif)

Good news - this part is done 🤣

#### Create a Basic Express.js Application

Since we are not replicating the Next.js server, but doing a simplified version of it, I will use the Express.js library to create the server and handle requests.

> I will use yarn package manager, but you're free to use anything you prefer

Install `express` and create a basic Express.js application

```bash
yarn add express
```

In the root of your project create a new file `server.js` and paste the following code:

```javascript
import express from 'express';

const app = express();
const port = 3000;

app.get('/', (req, res) => {
  res.send('Hello World!');
});

app.listen(port, () => {
  console.log(`Server is listening on port ${port}`);
});
```

Let's test it out. Start the server from the terminal `node server.js`


![Cannot use import outside of the modeule](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/liqjalohp326axxshkv2.png)

Congratulations! Our first error 😁

Node.js cannot use `import` by default, so in order to fix that, we will need to add a transpile step in our workflow.

> Trust me, this will help us in the future, so bear with me even if right now it looks like unnecessary comlpexity

To transpile our code, we will use [Babel](https://babeljs.io/) - a JavaScript compiler, that will generate files Node.js is happy with, and [Webpack](https://webpack.js.org/) - a JavaScript bundler, that will bundle our code and automate the compilation step.

**Configure Babel**

First, install all the necessary dependencies by running the following code:

```bash
yarn add -D @babel/core @babel/cli @babel/preset-env
```

This will add packages as `devDependencies`.

Now, at the root of your project create `.babelrc` file and paste the following configuration inside:

```yaml
{
  "presets": [
    "@babel/preset-env"
  ]
}
```

Let's test it! Run the following command inside your project:

```bash
npx babel server.js --out-file test-server.js
```

Babel will transpile our `server.js` file and create a new `test-server.js` file.

Start the project

```bash
node test-server.js
```

![Screenshot of successfully running Express app on port 3000](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m06shbtchj6rgx2qah5i.png)

If everything went well, congrats your server is started ✨

**Configure Webpack**

Okay, okay, this all may be a bit hard, but this is the last configuration step, I promise!

Install Webpack dependencies and `babel-loader` package

```bash
yarn add -D webpack webpack-cli webpack-node-externals babel-loader
```

Again, adding packages to your `devDependencies`.

In the root of your project create a new file `webpack.config.js` and paste the following code inside:

```javascript
const path = require('path');
const nodeExternals = require('webpack-node-externals');

module.exports = [
  // Server-side configuration
  {
    entry: './server.js',
    target: 'node', // Compiles for node.js environment
    externals: [nodeExternals()], // Excludes node_modules from the server bundle
    output: {
      filename: 'server.js',
      path: path.resolve(__dirname, 'dist'),
      publicPath: '/static/'
    },
    module: {
      rules: [
        {
          test: /\.(js|jsx)$/,
          exclude: /node_modules/,
          use: 'babel-loader'
        }
      ]
    },
    resolve: {
      extensions: ['.js', '.jsx']
    },
    mode: 'development'
  }
];
```

> Here we are simply telling Webpack to get entry file for server from the root folder and after everything is bundled store the final file inside dist folder.
> 
> Just try to read it top to bottom line by line and you'll see it 😉

And let's create a new script inside `package.json` file to run our app from a single command. Replace `"scripts"` object with the following code.

```javascript
// you can remove "test" script from the file

"scripts": {
  "build": "webpack --watch",
  "start": "nodemon dist/server.js"
},
```

> What does it do?
> 
> "build" - telling Webpack to bundle the code as per config and watch if entry files change, if yes - rebuild
> 
> "start" - nodemon watches server.js file inside dist folder and if it changes, restarts the server
> 
> P.S. if you don't have nodemon installed globally, add it as a dependency to the project - yarn add -D nodemon

So, shall we?

Run `yarn build` (or `npm run build` if you chose npm) - Webpack will generate the final bundled files.

Run `yarn start` - your server should be up and running!


![successfully built app](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a54mxfa75gh64on67r27.png)

Phew! That was a bit intense, but trust me, we would end up doing it anyway once we get to the React part.

**BACK TO THE FUN PART**

![Kid dancing fun](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/adpq9y7zfhfdkbazxevj.gif)

## First React Component

If you still remember what we are here for - good job. If not, I will remind you. We want to render React page on the server and for that, guess what, we need React component! Let's start with something very simple.

Inside the root of your project create a new folder `app` and subfolder `pages` - this is where we will keep our React page (we are mimicking Next.js after all)

![project structure](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vh4c4sdcczq85dasbpqc.png)

Let's install React and React-DOM

```bash
yarn add react react-dom
```

And also Babel React preset as `devDependency`

```bash
yarn add -D @babel/preset-react
```

Now our project is ready to use React. Inside `app/pages/index.jsx` create a very basic React component:

```javascript
import React from 'react';

export const HomePage = () => {
  return (
    <div>
      <h1>Home Page</h1>
    </div>
  );
}
```

Good, but, how do we render it? 🤔

### Rendering React Component on The Server

Luckily there's already a function for us available from the `react-dom` package - **renderToString** - you can read more about it here [**Server React DOM APIs**](https://react.dev/reference/react-dom/server/renderToString)**.**

As per React docs `renderToString` renders a React tree to an HTML string - exactly what we need in order to get HTML out of React component!

Let's change our Express app to use the function in order to render `HomePage` to string that we later can pass inside the HTML page.

#### Modifying Express App

My idea is that when a user visits the `/` route, `HomePage` the component will be rendered. For that, let's change the `/` route handler.

Inside the `server.js` file:

```javascript
app.get('/', (req, res) => {
  const htmlContent = renderToString(<HomePage />);
  res.send(`
    <!DOCTYPE html >
    <html lang="en">
      <head>
        <meta charset="UTF-8">
        <title>React SSR</title>
      </head>
      <body>
        <div id="root">${htmlContent}</div>
      </body>
    </html>
`);
});
```

Don't forget to import `renderToString` function and `HomePage` component.

```javascript
import { renderToString } from 'react-dom/server';
import { HomePage } from './app/pages';
```

So, let's see what it does step by step:

* `app.get('/', () => {})` - listens to index route (`/`) and calls the function when a GET request is received.
    
* `const htmlContent = renderToString(<HomePage />);` - using the `react-dom/server` function takes React component as an argument and renders it to an HTML string.
    
* ``res.send(`...`);`` - injects `htmlContent` inside template HTML string and sends as a response to the client
    

Check the diagram below to see the overview of this process.

![Overview of Client - Server communication](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/btt1fg90xak0gijwlaqe.png)

I bet you are eager to try it out!

**Well, let's try it 😉**


![Homepage error](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/znp5xk8qko8qc4kq488j.png)


![crying man](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q9coio88x5ea28e6w3ci.gif) 

Right...Webpack doesn't know about React. Let's fix it 🙌

Add the following to your `.babelrc` file:

```javascript
{
  "presets": [
    "@babel/preset-env",
    "@babel/preset-react" // <---- ADD THIS
  ]
}
```

**TRY AGAIN!**

![Successful build](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/udjrqmad2mg9y29i5jx9.png)

Alright, the build is successful! Now, let's start it and visit `localhost`!

```javascript
yarn start
```

Open your browser and navigate to `http://localhost:3000`


![Another error - React is not defined](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jygj0k4opzb28yi7fbbh.png)

Okay, okay, I won't torture you anymore. Basically, what is happening is that our `server.js` file cannot use React unless we import it and rename the file to `.jsx` too, so we can use JSX inside of it.

Let's fix that!

#### Fixing Express / React incompatibility

1. First, import React to your `server.js` file
    

```javascript
import express from 'express';
import React from 'react'; // <-- ADD THIS
import { renderToString } from 'react-dom/server';
import { HomePage } from './app/pages';
```

1. Next, rename `server.js` to `server.jsx`
    
2. And modify the Webpack config
    

```javascript
    entry: './server.jsx', // <--- CHANGE .js to .jsx
    target: 'node', 
    externals: [nodeExternals()],
    output: {
      filename: 'server.js',
      path: path.resolve(__dirname, 'dist'),
      publicPath: '/static/'
    },
```

Good, let's try it again.

```javascript
yarn build && yarn start
```

Go to `localhost:3000`


![Success](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7c7sppuj2tmtudrbuwop.png)

**YASSSSSS!**

Awesome! Now, we have legit *Server Side Rendered* React code! Congratulations!

![man dancing](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3qssk0xxzof3ep1p53e5.gif)

## Let's Make It Real React

We have managed to render React on the server side and ship it to the client. But...is it even React? 🥲

Since we are on our way to mimicking Next.js, let's mimic more of it! Something like `getServerSideProps` maybe? 👀

### notNextServerSideProps

Let's create a function that will be exported from our page and used to fetch data on *Server Side* too. Go to `app/pages/index.jsx` and add the following code outside of the `HomePage` component:

```javascript
export const notNextServerSideProps = async (fetch) => {
  const data = await fetch('https://fakestoreapi.com/products')
    .then(res => res.json())
    .then(json => json);


  return {
    props: {
      title: 'All Products',
      products: data
    }
  }
}
```

![X (ex Twitter) What is happening screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kf1t3bak04yhkww8slkz.png)

Inside the `notNextServerSideProps` function we are making a call to [Fake Store API](https://fakestoreapi.com/) - an open and free e-commerce store API. Here, we are simply getting a list of products.

Once we get the data, we return it as `props` object, adding custom `title`.

> In order to be able to use fetch in Node.js application, we will install node-fetch package ("node-fetch": "^2.6.7") and pass it as an argument.
> 
> This is a workaround(!), but for a simple prototype will do.

#### Calling notNextServerSideProps on The Server

First, install `node-fetch` the package. We will go with the version `2.6.7` since it is **commonjs** package and it will save us some time.

```javascript
yarn add -D node-fetch@2.6.7
```

Let's modify the `/` route handler and call `notNextServerSideProps` in order to fetch the data on the server.

```javascript
app.get('/', async (req, res) => {
  const initialData = await notNextServerSideProps(fetch);

  const htmlContent = renderToString(<HomePage {...initialData.props} />);
  res.send(`
    <!DOCTYPE html >
    <html lang="en">
      <head>
        <meta charset="UTF-8">
        <title>React SSR</title>
      </head>
      <body>
        <div id="root">${htmlContent}</div>
      </body>
    </html>
`);
});
```

* First, we made `app.get` callback an async function, so we can await a response from the Fake Store API
    
* `initialData` - data returned from `notNextServerSideProps` function
    
* `<HomePage {...initialData.props} />` - pass the `initialData.props` as props to `HomePage` component.
    

Don't forget to update your imports! (*I forgot while writing this article)*

```javascript
import { HomePage, notNextServerSideProps } from './app/pages'; // <--
const fetch = require('node-fetch'); //<-- using node-fetch library
```

#### Rendering React Component with Props

Now, we will need to change the `HomePage` component, so it can actually receive and use props inside.

```javascript
export const HomePage = ({ title }) => {
  return (
    <div>
      <h1>{title}</h1>
    </div>
  );
}
```

For now, we will keep it simple and render `title` only. Save files, build (`yarn build`) project and start it again, then visit `localhost:3000`.

![All products](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4wddg51tuo0xdy3zfw4u.png)

Good! The `notNextServerSideProps` function is running and passing props down to the React component, which then renders with the dynamic data.

Let's utilize the data we receive from Fake Store API then!

```javascript
export const HomePage = ({ title, products }) => {
  return (
    <div>
      <h1>{title}</h1>

      {products.map(product => (
        <div
          key={product.id}
          style={{
            display: 'flex',
            flexDirection: 'column',
            width: '200px',
            border: '1px solid black'
          }}>
          <p>{product.title}</p>
          <p>${product.price}</p>
          <p>{product.description}</p>
        </div>
      ))}
    </div>
  );
```

Here, we destructure `products` from `props` and map over them in order to display product data. We also add some inline style just to help us visualize it better (*it will be ugly - bear with me*).

Once again, build -&gt; start -&gt; `localhost:3000`.

![Server Side Rendered data fetched on server rendered on client](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gsdpmcjyznjvoi9mzn6h.png)

GOOOOOOOOOOOOOOD!

We have implemented a very hacky and very simplified version of Next.js **getServerSideProps** ourselves! How cool is that, huh?!

## I Said Real React
![X-men Magneto meme](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ygxy6kgzyqnabbxze381.gif)

React is not only about mapping over an array of data and rendering it. React is all about *'reactivity'* (pun intended). And this is what our app is lacking at the moment.

But, how can we add reactivity to the page if we don't have any JavaScript inside the HTML page and especially nothing close to React itself - this is where Hydration comes into play.

### What is Hydration?

As per ChatGPT

> **Hydration**: To make this static content interactive, React needs to attach event listeners and establish its internal representation of the page. This process is called "hydration." During hydration, React will preserve the server-rendered markup and attach event handlers to it, effectively turning the static content into a dynamic React application.

What it means is that React will take over the rendering inside the browser, use the HTML and data provided by the server and make things clickable, interactive, and *reactive*.

Luckily for us (**again**) React team has another function for us called `hydrateRoot`. You can read more about it here - [React Client APIs](https://react.dev/reference/react-dom/client/hydrateRoot).

It's very easy to use this function. We need to create an entry point into our Client application, get the `root` element of our app and using `hydrateRoot` - hydrate components into the `root` element (sorry for the tautology).

#### Create Client Application

To create an entry point to our application, inside `app` folder, let's create `app.jsx` file and write some code inside:

```javascript
import React from 'react';
import { hydrateRoot } from 'react-dom/client';
import { HomePage } from './pages';

const domNode = document.getElementById('root');
hydrateRoot(domNode, <HomePage />);
```

As we learned before, we get `root` element from the DOM and hydrate component inside it using `hydrateRoot` function.

But this will lead to an error, many errors to be honest, but let's start one by one.

> It's extremely important that SSR React output (HTML) and CSR React output (HTML) are matching, otherwise React will not be able to render and attach event listeners properly.

In order to make sure that the data available to `HomePage` the component is exactly the same, Next.js injects this data as a global variable inside `window` the object as part of the HTML it returns to the client.

Let's do it together but before that!

**Extract HTML Template in The Separate Function**

So far we were injecting `htmlContent` directly in the string inside the `/` route handler. This is of course not optimal, since the more routes we get, the more HTML templates we will have hardcoded in our project.

To avoid that and make the template more versatile, let's create `document.js` file and `document` function inside it.

> By the way, this will be simplified version of Next.js \_document.js file

Create `utils/document.js` file in the root of your project and paste the following code:

```javascript
export const document = (htmlContent) => {
  return `
    <!DOCTYPE html >
    <html lang="en">
      <head>
        <meta charset="UTF-8">
        <title>React SSR</title>
      </head>
      <body>
        <div id="root">${htmlContent}</div>
      </body>
    </html>
    `;
}
```

Now, go back to `server.jsx` and change the route handler:

```javascript
//...
import { document } from './utils/document';
//...

app.get('/', async (req, res) => {
  const initialData = await notNextServerSideProps(fetch);

  const htmlContent = renderToString(<HomePage {...initialData.props} />);
  const html = document(htmlContent);
  res.send(html);
});
```

We can reuse this function for future pages!

#### Pass Initial Data to HTML

Let's add `initialData` that we receive from the `notNextServerSideProps` function and inject it into HTML. For this, we need to pass it on to the `document` function and then add it as a `<script>` tag inside HTML template.

Open `document.js` file and replace the existing code with the new one:

```javascript
export const document = (htmlContent, initialData) => {
  return `
    <!DOCTYPE html >
    <html lang="en">
      <head>
        <meta charset="UTF-8">
        <title>React SSR</title>
      </head>
      <body>
        <div id="root">${htmlContent}</div>
        <script>window.__SSR_DATA__ = ${JSON.stringify(initialData)}</script>
      </body>
    </html>
    `;
}
```

* `initialData` - added `initialData` as an argument to the function
    
* `<script>` - inside this script tag, we set a new property to `window` object - `__SSR_DATA__` that will be available globally inside the app. `initialData` has to be stringified in order to be transferred across the network.
    

Now, when the Client Application is loaded in the `root` of our HTML, it can access the `initialData` and use it in order to hydrate the component!

And add `initialData.props` to the `document` function inside route handler:

```javascript
app.get('/', async (req, res) => {
  const initialData = await notNextServerSideProps(fetch);

  const htmlContent = renderToString(<HomePage {...initialData.props} />);
  const html = document(htmlContent, initialData.props);
  res.send(html);
});
```

#### Adding `initialData` to HomePage Component

Since the data is available in the `window` object, we can easily access it from the Client Application.

Open `app.jsx` and add the following:

```javascript
/* imports */

const initialProps = window.__SSR_DATA__;

const domNode = document.getElementById('root');
hydrateRoot(domNode, <HomePage {...initialProps} />);
```

* getting `initialProps` from the `window` object
    
* passing `initialProps` as `HomePage` props
    

Now, when React will hydrate the component on the Client Side, it will have access to the same exact data as the Server and the contents will match.

### Running Client Application

Our latest challenge is to run a Client Application because so far, we are only rendering HTML on the server and sending it to the browser, but we don't really run React app in the browser.

> Remember I said "no more Webpack"? I LIED.

First of all, we need to transpile our React code into browser-readable code using Babel and Webpack, so all imports are bundled together.

Inside your `webpack.config.js` paste the following before or after the server-side config:

```javascript
{
    entry: './app/app.jsx', // Entry point for your client-side code
    output: {
      filename: 'app.js',
      path: path.resolve(__dirname, 'dist'),
      publicPath: '/static/' // Important for dynamic imports to know where to fetch bundles
    },
    module: {
      rules: [
        {
          test: /\.(js|jsx)$/,
          exclude: /node_modules/,
          use: 'babel-loader'
        }
      ]
    },
    resolve: {
      extensions: ['.js', '.jsx']
    },
    mode: 'development'
},
```

Again, no magic here, just simple "where grab the file" and "where to put this file".

Once this is done, Webpack will transpile and bundle `app.jsx` to `app.js` and keep it in the `dist` folder.

Now, we need to actually make our HTML request for the Client Application aka `app.js`. Express.js provides us with a simple middleware to set up a static folder, from where HTML can request files. Let's do that.

```javascript
app.use('/static', express.static(path.join(__dirname)));
// THIS CODE SHOULD BE BEFORE app.get

// app.get('/', async (req, res) => {
```

Since the output file (after transpile and bundling) will be located inside `dist` folder (`dist/server.js`), we set `static` middleware to point to the same directory.

> don't forget to import path 😉 (yes I forgot again)

Now, when HTML requests for static content (JS, CSS, images) it will be able to call `/static` route and get what it needs.

The very last step is to add `app.js` to the HTML. Open your `document.js` and add the following after the `initialData` script tag.

```javascript
<script src="/static/app.js"></script>
```

Everything is ready! Let's try it out!

build -&gt; start -&gt; `localhost:3000`


![Product list page with React hydration](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/haoj1jafrd9udn7rx74t.png)

Everything looks the same as before...But, there are no errors and it means that everything worked! Test time!

## Testing SSR React Application

Let's make a simple test!

We will create a counter on top of the page and for every product card add a button that `onClick` will console log product name.

Go back to `app/pages/index.jsx` and replace `HomePage` component:

```javascript
export const HomePage = ({ title, products }) => {
  const [count, setCount] = useState(0);

  return (
    <div>
      <h1>{title}</h1>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>

      <div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
        {products.map(product => (
          <div
            key={product.id}
            style={{
              display: 'flex',
              flexDirection: 'column',
              width: '200px',
              border: '1px solid black'
            }}>
            <p>{product.title}</p>
            <p>${product.price}</p>
            <p>{product.description}</p>

            <button onClick={() => console.log(product.title)}>Console</button>
          </div>
        ))}
      </div>
    </div>
  );
}
```

Don't forget to import `useState` 😅

```javascript
import React, { useState } from 'react';
```

Okay! Fingers crossed!

build -&gt; start -&gt; `localhost:3000`


![Gif showing the completed application](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jr4cskcfyqd0x8pyt9x4.gif)

BOOM! 💥

We have created a very simple, but working React Application with *Server Side Rendering*!

Almost as good as Next.js itself 😁 Gret job!


![BOOM gif image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rmgr30yzvmy1lxhov29j.gif)

## Conclusions

No conclusions 😁

You've done a great job coming so far! Round of applause for you!

We dug a bit deeper into the hows of Next.js and next time, we will go deeper or broader. How about we dive deep into how App Router works under the hood? 👀

Ask your questions below if you have any and please share this article with those who have doubts on Next.js SSR.

Find me on X 👋

{% embed https://twitter.com/SergiiKirianov/status/1693037630890406064?s=20 %}