How to Implement SSR with Express.js and React

Learn how to implement Server-Side Rendering (SSR) with Express.js and React. Follow our guide for improved performance and SEO in your React applications.

Server-Side Rendering (SSR) is a technique that allows developers to render web pages on the server instead of the client’s browser. This approach can significantly improve the performance and SEO of your web applications. When combined with Express.js and React, SSR can create robust and highly responsive web applications. In this article, we will dive deep into implementing SSR with Express.js and React, ensuring a seamless and efficient process.

Understanding SSR and Its Benefits

SSR stands for Server-Side Rendering. In traditional client-side rendering, the entire application runs in the browser. The server sends a basic HTML file with minimal content, and the browser uses JavaScript to build the content dynamically.

What is SSR?

SSR stands for Server-Side Rendering. In traditional client-side rendering, the entire application runs in the browser. The server sends a basic HTML file with minimal content, and the browser uses JavaScript to build the content dynamically.

SSR, on the other hand, pre-renders the initial HTML on the server and sends it to the client. This pre-rendered HTML can significantly improve the initial load time of your web application.

Benefits of SSR

  1. Improved Performance: By pre-rendering HTML on the server, the browser can display content much faster, leading to a better user experience.
  2. SEO Advantages: Search engines can easily crawl pre-rendered HTML, improving the discoverability of your web application.
  3. Enhanced User Experience: Users can see the content quicker, reducing the perceived load time and enhancing overall satisfaction.

Setting Up the Environment

Before we dive into the implementation, let’s set up our environment. We will need Node.js, Express.js, and React. If you haven’t already, install Node.js from the official website.

Initializing a New Project

First, create a new directory for your project and navigate into it:

mkdir ssr-express-react
cd ssr-express-react

Next, initialize a new Node.js project:

npm init -y

This command will create a package.json file in your project directory. Now, install the necessary dependencies:

npm install express react react-dom @babel/core @babel/preset-env @babel/preset-react babel-loader

Setting Up Babel

Babel is a JavaScript compiler that allows us to use modern JavaScript features and JSX syntax. Create a .babelrc file in the root of your project with the following content:

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

This configuration tells Babel to use the @babel/preset-env for modern JavaScript features and @babel/preset-react for JSX.

Creating the Server

Now, let’s create a basic Express server. In your project directory, create a new file named server.js and add the following code:

const express = require('express');
const path = require('path');
const app = express();
const PORT = process.env.PORT || 3000;

app.use(express.static(path.resolve(__dirname, 'public')));

app.get('*', (req, res) => {
  res.sendFile(path.resolve(__dirname, 'public', 'index.html'));
});

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

This code sets up an Express server that serves static files from a public directory and sends index.html for all routes.

Setting Up Webpack

Webpack is a module bundler that will help us bundle our React application. Create a webpack.config.js file in the root of your project with the following content:

const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'public'),
    filename: 'bundle.js'
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader'
        }
      }
    ]
  },
  resolve: {
    extensions: ['.js', '.jsx']
  },
  mode: 'development'
};

This configuration specifies the entry point of our React application, the output location for the bundled file, and the rules for handling JavaScript and JSX files.

Building the React Application

Let's create the React application structure. Inside your project directory, create a new directory named src and add the following files:

Let’s create the React application structure. Inside your project directory, create a new directory named src and add the following files:

  • index.js
  • App.js

index.js

The index.js file will be the entry point of our React application. Add the following code:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.hydrate(<App />, document.getElementById('root'));

App.js

The App.js file will contain the main component of our React application. Add the following code:

import React from 'react';

const App = () => {
  return (
    <div>
      <h1>Hello, SSR with Express and React!</h1>
    </div>
  );
};

export default App;

This basic React application will render a simple “Hello, SSR with Express and React!” message.

Integrating SSR

Now that we have our basic setup, let’s integrate SSR. Update the server.js file to include the following changes:

const express = require('express');
const path = require('path');
const React = require('react');
const ReactDOMServer = require('react-dom/server');
const { StaticRouter } = require('react-router-dom/server');
const App = require('./src/App').default;

const app = express();
const PORT = process.env.PORT || 3000;

app.use(express.static(path.resolve(__dirname, 'public')));

app.get('*', (req, res) => {
  const context = {};
  const appMarkup = ReactDOMServer.renderToString(
    <StaticRouter location={req.url} context={context}>
      <App />
    </StaticRouter>
  );

  const html = `
    <!DOCTYPE html>
    <html>
      <head>
        <title>SSR with Express and React</title>
      </head>
      <body>
        <div id="root">${appMarkup}</div>
        <script src="/bundle.js"></script>
      </body>
    </html>
  `;

  res.send(html);
});

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

This code uses ReactDOMServer.renderToString to pre-render our React application on the server. It also uses StaticRouter from react-router-dom to handle routing on the server.

Handling Routing and Data Fetching

To manage client-side routing, we need to set up React Router. Install react-router-dom:

Setting Up React Router

To manage client-side routing, we need to set up React Router. Install react-router-dom:

npm install react-router-dom

Update the App.js file to include routing:

import React from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';

const Home = () => <h1>Home Page</h1>;
const About = () => <h1>About Page</h1>;

const App = () => {
  return (
    <Router>
      <Switch>
        <Route exact path="/" component={Home} />
        <Route path="/about" component={About} />
      </Switch>
    </Router>
  );
};

export default App;

In this example, we added two routes: Home and About.

Handling Routing on the Server

Update the server.js file to handle routing correctly on the server:

const express = require('express');
const path = require('path');
const React = require('react');
const ReactDOMServer = require('react-dom/server');
const { StaticRouter } = require('react-router-dom/server');
const App = require('./src/App').default;

const app = express();
const PORT = process.env.PORT || 3000;

app.use(express.static(path.resolve(__dirname, 'public')));

app.get('*', (req, res) => {
  const context = {};
  const appMarkup = ReactDOMServer.renderToString(
    <StaticRouter location={req.url} context={context}>
      <App />
    </StaticRouter>
  );

  if (context.url) {
    res.redirect(301, context.url);
  } else {
    const html = `
      <!DOCTYPE html>
      <html>
        <head>
          <title>SSR with Express and React</title>
        </head>
        <body>
          <div id="root">${appMarkup}</div>
          <script src="/bundle.js"></script>
        </body>
      </html>
    `;

    res.send(html);
  }
});

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

This setup ensures that routing works seamlessly on both the server and client sides. The StaticRouter component from react-router-dom/server handles server-side routing, and BrowserRouter handles client-side routing.

Fetching Data on the Server

Fetching data on the server can be a bit tricky, but it’s essential for SSR. Let’s create a simple data fetching example.

First, install axios:

npm install axios

Create a new file named api.js in the src directory:

import axios from 'axios';

export const fetchData = async () => {
  const response = await axios.get('https://jsonplaceholder.typicode.com/posts/1');
  return response.data;
};

Update the Home component in App.js to fetch data:

import React, { useEffect, useState } from 'react';
import { fetchData } from './api';

const Home = () => {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetchData().then((result) => {
      setData(result);
    });
  }, []);

  return (
    <div>
      <h1>Home Page</h1>
      {data ? <p>{data.title}</p> : <p>Loading...</p>}
    </div>
  );
};

Fetching Data on the Server

Update the server.js file to fetch data on the server:

const express = require('express');
const path = require('path');
const React = require('react');
const ReactDOMServer = require('react-dom/server');
const { StaticRouter } = require('react-router-dom/server');
const App = require('./src/App').default;
const { fetchData } = require('./src/api');

const app = express();
const PORT = process.env.PORT || 3000;

app.use(express.static(path.resolve(__dirname, 'public')));

app.get('*', async (req, res) => {
  const context = {};
  const data = await fetchData();

  const appMarkup = ReactDOMServer.renderToString(
    <StaticRouter location={req.url} context={context}>
      <App initialData={data} />
    </StaticRouter>
  );

  if (context.url) {
    res.redirect(301, context.url);
  } else {
    const html = `
      <!DOCTYPE html>
      <html>
        <head>
          <title>SSR with Express and React</title>
        </head>
        <body>
          <div id="root">${appMarkup}</div>
          <script>
            window.__INITIAL_DATA__ = ${JSON.stringify(data)};
          </script>
          <script src="/bundle.js"></script>
        </body>
      </html>
    `;

    res.send(html);
  }
});

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

This setup fetches data on the server and passes it to the client through window.__INITIAL_DATA__. Update the Home component to use this initial data:

import React, { useEffect, useState } from 'react';
import { fetchData } from './api';

const Home = ({ initialData }) => {
  const [data, setData] = useState(initialData);

  useEffect(() => {
    if (!initialData) {
      fetchData().then((result) => {
        setData(result);
      });
    }
  }, [initialData]);

  return (
    <div>
      <h1>Home Page</h1>
      {data ? <p>{data.title}</p> : <p>Loading...</p>}
    </div>
  );
};

export default Home;

By passing initialData as a prop, we ensure that the component is hydrated with server-rendered data, improving performance and SEO.

Optimizing Your SSR Setup

Caching

Caching can significantly improve the performance of your SSR application. Implementing a simple cache in Express is straightforward. Install the node-cache package:

npm install node-cache

Update server.js to use caching:

const express = require('express');
const path = require('path');
const React = require('react');
const ReactDOMServer = require('react-dom/server');
const { StaticRouter } = require('react-router-dom/server');
const App = require('./src/App').default;
const { fetchData } = require('./src/api');
const NodeCache = require('node-cache');

const app = express();
const cache = new NodeCache({ stdTTL: 100 });
const PORT = process.env.PORT || 3000;

app.use(express.static(path.resolve(__dirname, 'public')));

app.get('*', async (req, res) => {
  const cacheKey = req.url;
  if (cache.has(cacheKey)) {
    return res.send(cache.get(cacheKey));
  }

  const context = {};
  const data = await fetchData();

  const appMarkup = ReactDOMServer.renderToString(
    <StaticRouter location={req.url} context={context}>
      <App initialData={data} />
    </StaticRouter>
  );

  if (context.url) {
    res.redirect(301, context.url);
  } else {
    const html = `
      <!DOCTYPE html>
      <html>
        <head>
          <title>SSR with Express and React</title>
        </head>
        <body>
          <div id="root">${appMarkup}</div>
          <script>
            window.__INITIAL_DATA__ = ${JSON.stringify(data)};
          </script>
          <script src="/bundle.js"></script>
        </body>
      </html>
    `;

    cache.set(cacheKey, html);
    res.send(html);
  }
});

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

This caching setup stores the HTML response for each URL and serves it directly if it’s already cached, reducing the need to re-render the same content.

Code Splitting

Code splitting helps reduce the initial load time by splitting your application into smaller chunks. Install react-loadable:

npm install @loadable/component

Update App.js to use code splitting:

import React from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import loadable from '@loadable/component';

const Home = loadable(() => import('./Home'));
const About = loadable(() => import('./About'));

const App = () => {
  return (
    <Router>
      <Switch>
        <Route exact path="/" component={Home} />
        <Route path="/about" component={About} />
      </Switch>
    </Router>
  );
};

export default App;

Enhancing the User Experience

Lazy loading further enhances the user experience by loading components only when they are needed. This approach can be particularly beneficial for large applications.

Implementing Lazy Loading

Lazy loading further enhances the user experience by loading components only when they are needed. This approach can be particularly beneficial for large applications.

Lazy Loading Components

We have already set up code splitting using @loadable/component. Now let’s enhance our lazy loading setup. Create two new components, Home.js and About.js:

Home.js

import React, { useEffect, useState } from 'react';
import { fetchData } from './api';

const Home = ({ initialData }) => {
  const [data, setData] = useState(initialData);

  useEffect(() => {
    if (!initialData) {
      fetchData().then((result) => {
        setData(result);
      });
    }
  }, [initialData]);

  return (
    <div>
      <h1>Home Page</h1>
      {data ? <p>{data.title}</p> : <p>Loading...</p>}
    </div>
  );
};

export default Home;

About.js

import React from 'react';

const About = () => {
  return (
    <div>
      <h1>About Page</h1>
      <p>This is the about page.</p>
    </div>
  );
};

export default About;

Updating App.js

Update App.js to use the lazy-loaded components:

import React from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import loadable from '@loadable/component';

const Home = loadable(() => import('./Home'));
const About = loadable(() => import('./About'));

const App = ({ initialData }) => {
  return (
    <Router>
      <Switch>
        <Route
          exact
          path="/"
          render={(props) => <Home {...props} initialData={initialData} />}
        />
        <Route path="/about" component={About} />
      </Switch>
    </Router>
  );
};

export default App;

This setup ensures that the Home and About components are loaded only when needed, reducing the initial bundle size.

Adding Styles

Styling your application is essential for a polished look and feel. Let’s add some basic styling to our application. We will use CSS modules for this purpose.

Setting Up CSS Modules

First, install the necessary dependencies:

npm install style-loader css-loader

Update webpack.config.js to handle CSS modules:

const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'public'),
    filename: 'bundle.js'
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader'
        }
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      }
    ]
  },
  resolve: {
    extensions: ['.js', '.jsx']
  },
  mode: 'development'
};

Creating CSS Modules

Create a new file named App.module.css in the src directory:

.container {
  font-family: Arial, sans-serif;
}

h1 {
  color: #333;
}

p {
  font-size: 16px;
}

Applying Styles

Update App.js to use the styles:

import React from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import loadable from '@loadable/component';
import styles from './App.module.css';

const Home = loadable(() => import('./Home'));
const About = loadable(() => import('./About'));

const App = ({ initialData }) => {
  return (
    <div className={styles.container}>
      <Router>
        <Switch>
          <Route
            exact
            path="/"
            render={(props) => <Home {...props} initialData={initialData} />}
          />
          <Route path="/about" component={About} />
        </Switch>
      </Router>
    </div>
  );
};

export default App;

Styling Individual Components

Update Home.js and About.js to use styles:

Home.js

import React, { useEffect, useState } from 'react';
import { fetchData } from './api';
import styles from './Home.module.css';

const Home = ({ initialData }) => {
  const [data, setData] = useState(initialData);

  useEffect(() => {
    if (!initialData) {
      fetchData().then((result) => {
        setData(result);
      });
    }
  }, [initialData]);

  return (
    <div className={styles.home}>
      <h1>Home Page</h1>
      {data ? <p>{data.title}</p> : <p>Loading...</p>}
    </div>
  );
};

export default Home;

About.js

import React from 'react';
import styles from './About.module.css';

const About = () => {
  return (
    <div className={styles.about}>
      <h1>About Page</h1>
      <p>This is the about page.</p>
    </div>
  );
};

export default About;

Deploying Your SSR Application

Preparing for Production

Before deploying, we need to prepare our application for production. Update the webpack.config.js file to include a production mode configuration:

const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'public'),
    filename: 'bundle.js'
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader'
        }
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      }
    ]
  },
  resolve: {
    extensions: ['.js', '.jsx']
  },
  mode: process.env.NODE_ENV === 'production' ? 'production' : 'development'
};

Building the Application

Build your application for production by running the following command:

NODE_ENV=production webpack --mode=production

This command will generate an optimized bundle.js in the public directory.

Deploying to a Server

You can deploy your SSR application to any server that supports Node.js. For simplicity, we’ll use Heroku for deployment. If you don’t have the Heroku CLI installed, you can download it here.

Step-by-Step Deployment on Heroku

  1. Login to Heroku: heroku login
  2. Create a New Heroku Application: heroku create your-app-name
  3. Commit Your Changes: git add . git commit -m "Initial commit"
  4. Deploy to Heroku: git push heroku main
  5. Open Your Deployed Application: heroku open

Congratulations! Your SSR application is now live on Heroku.

Advanced Techniques for SSR

Using Redux for State Management

State management is crucial for complex applications. Redux is a popular library for managing state in React applications. In this section, we’ll integrate Redux into our SSR setup.

Setting Up Redux

First, install Redux and React-Redux:

npm install redux react-redux

Create a new file named store.js in the src directory:

import { createStore } from 'redux';

const initialState = {
  data: null,
};

const reducer = (state = initialState, action) => {
  switch (action.type) {
    case 'SET_DATA':
      return {
        ...state,
        data: action.data,
      };
    default:
      return state;
  }
};

const store = createStore(reducer);

export default store;

Connecting Redux with React

Update App.js to connect Redux with React:

import React from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import loadable from '@loadable/component';
import { Provider } from 'react-redux';
import store from './store';
import styles from './App.module.css';

const Home = loadable(() => import('./Home'));
const About = loadable(() => import('./About'));

const App = ({ initialData }) => {
  return (
    <Provider store={store}>
      <div className={styles.container}>
        <Router>
          <Switch>
            <Route
              exact
              path="/"
              render={(props) => <Home {...props} initialData={initialData} />}
            />
            <Route path="/about" component={About} />
          </Switch>
        </Router>
      </div>
    </Provider>
  );
};

export default App;

Updating Home Component to Use Redux

Modify Home.js to dispatch and access state from Redux:

import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchData } from './api';
import styles from './Home.module.css';

const Home = ({ initialData }) => {
  const dispatch = useDispatch();
  const data = useSelector((state) => state.data);

  useEffect(() => {
    if (!initialData) {
      fetchData().then((result) => {
        dispatch({ type: 'SET_DATA', data: result });
      });
    } else {
      dispatch({ type: 'SET_DATA', data: initialData });
    }
  }, [initialData, dispatch]);

  return (
    <div className={styles.home}>
      <h1>Home Page</h1>
      {data ? <p>{data.title}</p> : <p>Loading...</p>}
    </div>
  );
};

export default Home;

Updating Server to Use Redux

Update server.js to provide initial state to Redux:

const express = require('express');
const path = require('path');
const React = require('react');
const ReactDOMServer = require('react-dom/server');
const { StaticRouter } = require('react-router-dom/server');
const { Provider } = require('react-redux');
const App = require('./src/App').default;
const { fetchData } = require('./src/api');
const configureStore = require('./src/store').default;
const NodeCache = require('node-cache');

const app = express();
const cache = new NodeCache({ stdTTL: 100 });
const PORT = process.env.PORT || 3000;

app.use(express.static(path.resolve(__dirname, 'public')));

app.get('*', async (req, res) => {
  const cacheKey = req.url;
  if (cache.has(cacheKey)) {
    return res.send(cache.get(cacheKey));
  }

  const context = {};
  const data = await fetchData();
  const store = configureStore({ data });

  const appMarkup = ReactDOMServer.renderToString(
    <Provider store={store}>
      <StaticRouter location={req.url} context={context}>
        <App initialData={data} />
      </StaticRouter>
    </Provider>
  );

  if (context.url) {
    res.redirect(301, context.url);
  } else {
    const html = `
      <!DOCTYPE html>
      <html>
        <head>
          <title>SSR with Express and React</title>
        </head>
        <body>
          <div id="root">${appMarkup}</div>
          <script>
            window.__INITIAL_DATA__ = ${JSON.stringify(data)};
          </script>
          <script src="/bundle.js"></script>
        </body>
      </html>
    `;

    cache.set(cacheKey, html);
    res.send(html);
  }
});

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

Adding User Authentication

User authentication is a common requirement for many web applications. Let’s add basic user authentication using JWT (JSON Web Tokens).

Setting Up JWT

First, install the necessary packages:

npm install jsonwebtoken bcryptjs

Create a new file named auth.js in the src directory:

const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');

const users = [
  {
    id: 1,
    username: 'user1',
    password: bcrypt.hashSync('password1', 8),
  },
];

const authenticate = (username, password) => {
  const user = users.find((u) => u.username === username);
  if (user && bcrypt.compareSync(password, user.password)) {
    const token = jwt.sign({ id: user.id }, 'your_jwt_secret', {
      expiresIn: 86400, // 24 hours
    });
    return token;
  }
  return null;
};

const verifyToken = (token) => {
  try {
    return jwt.verify(token, 'your_jwt_secret');
  } catch (err) {
    return null;
  }
};

module.exports = {
  authenticate,
  verifyToken,
};

Creating Authentication Routes

Update server.js to include authentication routes:

const express = require('express');
const path = require('path');
const React = require('react');
const ReactDOMServer = require('react-dom/server');
const { StaticRouter } = require('react-router-dom/server');
const { Provider } = require('react-redux');
const App = require('./src/App').default;
const { fetchData } = require('./src/api');
const configureStore = require('./src/store').default;
const NodeCache = require('node-cache');
const bodyParser = require('body-parser');
const { authenticate, verifyToken } = require('./src/auth');

const app = express();
const cache = new NodeCache({ stdTTL: 100 });
const PORT = process.env.PORT || 3000;

app.use(express.static(path.resolve(__dirname, 'public')));
app.use(bodyParser.json());

app.post('/login', (req, res) => {
  const { username, password } = req.body;
  const token = authenticate(username, password);
  if (token) {
    res.json({ token });
  } else {
    res.status(401).json({ error: 'Invalid credentials' });
  }
});

app.get('*', async (req, res) => {
  const cacheKey = req.url;
  if (cache.has(cacheKey)) {
    return res.send(cache.get(cacheKey));
  }

  const context = {};
  const data = await fetchData();
  const store = configureStore({ data });

  const appMarkup = ReactDOMServer.renderToString(
    <Provider store={store}>
      <StaticRouter location={req.url} context={context}>
        <App initialData={data} />
      </StaticRouter>
    </Provider>
  );

  if (context.url) {
    res.redirect(301, context.url);
  } else {
    const html = `
      <!DOCTYPE html>
      <html>
        <head>
          <title>SSR with Express and React</title>
        </head>
        <body>
          <div id="root">${appMarkup}</div>
          <script>
            window.__INITIAL_DATA__ = ${JSON.stringify(data)};
          </script>
          <script src="/bundle.js"></script>
        </body>
      </html>
    `;

    cache.set(cacheKey, html);
    res.send(html);
  }
});

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

Protecting Routes

Create a new file named protectedRoute.js in the src directory:

import React from 'react';
import { Route, Redirect } from 'react-router-dom';

const ProtectedRoute = ({ component: Component, ...rest }) => {
  const isAuthenticated = !!localStorage.getItem('token');
  return (
    <Route
      {...rest}
      render={(props) =>
        isAuthenticated ? <Component {...props} /> : <Redirect to="/login" />
      }
    />
  );
};

export default ProtectedRoute;

Creating Login Component

Create a new file named Login.js in the src directory:

import React, { useState } from 'react';
import axios from 'axios';
import { useHistory } from 'react-router-dom';
import styles from './Login.module.css';

const Login = () => {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');
  const history = useHistory();

  const handleSubmit = async (event) => {
    event.preventDefault();
    try {
      const response = await axios.post('/login', { username, password });
      localStorage.setItem('token', response.data.token);
      history

.push('/');
    } catch (error) {
      alert('Invalid credentials');
    }
  };

  return (
    <div className={styles.login}>
      <h1>Login</h1>
      <form onSubmit={handleSubmit}>
        <div>
          <label>Username</label>
          <input
            type="text"
            value={username}
            onChange={(e) => setUsername(e.target.value)}
          />
        </div>
        <div>
          <label>Password</label>
          <input
            type="password"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
          />
        </div>
        <button type="submit">Login</button>
      </form>
    </div>
  );
};

export default Login;

Updating App.js for Authentication

Update App.js to include the login route and protected routes:

import React from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import loadable from '@loadable/component';
import { Provider } from 'react-redux';
import store from './store';
import ProtectedRoute from './protectedRoute';
import styles from './App.module.css';

const Home = loadable(() => import('./Home'));
const About = loadable(() => import('./About'));
const Login = loadable(() => import('./Login'));

const App = ({ initialData }) => {
  return (
    <Provider store={store}>
      <div className={styles.container}>
        <Router>
          <Switch>
            <Route path="/login" component={Login} />
            <ProtectedRoute
              exact
              path="/"
              component={(props) => <Home {...props} initialData={initialData} />}
            />
            <ProtectedRoute path="/about" component={About} />
          </Switch>
        </Router>
      </div>
    </Provider>
  );
};

export default App;

Conclusion

Implementing SSR with Express.js and React, combined with advanced techniques such as Redux for state management and JWT for user authentication, can significantly enhance the performance, security, and user experience of your web applications. This guide has covered the setup and implementation of these features, ensuring a robust and efficient SSR application.

By following this comprehensive guide, you have now built an SSR application with state management, routing, data fetching, authentication, and optimized performance. These techniques will help you create scalable and high-performing web applications that provide a seamless experience for your users.

Read Next: