Skip to content

Server-Side Rendered App with Next.js, React and Redux

There are many known benefits to pre-rendering your web application or rendering your web app on the server side, some of which include better SEO, faster-load times, a better user experience for users with poor connections, and many more.

This post will guide you towards getting quickly started with Next and using it to develop a React-Redux web application.

Note: This post is outdated and was written for Next.js versions 9.2 and lower. Please refer to next-redux-wrapper to see how to create an updated version Next.js-redux web app.

Prerequisites for this post:

  • Understanding of Basic React Concepts
  • Knowledge of Basic Redux Concepts

After going through all the concepts for this post, we will create a simple counter app with our server-side rendered app.

Getting Started with Next.JS

Next.js is a React-Framework that makes it easy to develop react server-side rendered apps. It also provides additional features, but in this post, we will only go over rendering applications server-side with Next.js.

I highly recommend going over the docs. This part goes over the basic principles of the next, and there is a lot of overlap with the documentation. I recommend reviewing the documentation and proceeding to this article’s next part. Nevertheless, if the documentation is insufficient, you can keep reading!

If you already have Next.js installed and know the basics, you can skip to the following

To get started, we first create a project directory:

mkdir hello-next

We then initialize the project with npm:

cd hello-next
npm init -y

We then need to install next, react & react-dom, these are necessary dependencies for next:

npm install --save react react-dom next

We then create a ‘pages’ directory within our project directory. Any React files in this directory by default are mapped to the URL routes based on the file name for our server-side rendered app:

mkdir pages

A file name index.jsx will be mapped to the root URL, i.e., localhost:3000/.
Similarly, a file called login.jsx will be mapped to localhost:3000/login
This feature is enabled by default, which is sufficient for our use case.

To get started with next, we need to edit our package.json in our project directory and replace the scripts with the following:

"scripts": {
  "dev": "next",
  "build": "next build",
  "start": "next start"
}

After doing so, everything is ready. You can now run this command in the project directory:

npm run dev

After a few seconds, the development server should be up and running and visiting localhost:3000 will yield “404 | Page Not Found”. This is because our pages directory does not have an “index.jsx” file. We can proceed to create it:

touch pages/index.jsx

We can then create a simple Hello-World index page:

import React from 'react';

class App extends React.Component {
    render() {
        return (
            <h1> Hello World! </h1>
        );
    }
}

export default App;

Here we make a React Component that renders “Hello World” and visiting the root path will show the result.

Next only recognizes the default imports in the React files in the pages directory and will only render the default Component when browsed to the URL path.

Creating a Simple Counter App (Without Redux)

To create a simple counter app, we can do the following:

import React from 'react';

class App extends React.Component {

    constructor(props) {
        super(props);

        //Initialise state
        this.state = {
            counter: 0
        };
    }

    //Updates the counter in state by +1
    incrementCounter = () => {
        this.setState(prevState => {
            this.setState({
                counter: prevState.counter + 1.
            });
        });
    };

    //Updates the counter in state by  -1
    decrementCounter = () => {
        this.setState(prevState => {
            this.setState({
                counter: prevState.counter - 1.
            });
        });
    };

    render() {
        return (
            <div>
                <button onClick={this.incrementCounter}>Increment</button>
                <button onClick={this.decrementCounter}>Decrement</button>
                <h1>{this.state.counter}</h1>
            </div>
        );
    }
}

export default App;

Clicking on the appropriate buttons increment and decrements.

As you can see, next makes use of React, so working with next is simple just working with React. The only difference is that next automatically (Behind-the-scenes) renders the application server side.

Understanding Server-Side Rendering with Redux

Similar to how working with Next is just working with React. Redux web apps behave in the same way. Everything works similarly to how it would work if the app were rendered on the client side. The only challenge working with Redux is the initial Redux setup with server-side rendering, and this is precisely what the following part covers.

The official documentation, Redux, explains how server-side rendering is expected to work with Redux. The explanation states that:

When using Redux with server rendering, we must also send the state of our app in our response so that the client can use it as the initial state. This is important because if we preload any data before generating the HTML, we want the client also to have access to this data. Otherwise, the markup generated on the client won’t match the server markup, and the client would have to load the data again.

To send the data down to the client, we need to:

  • create a fresh, new Redux store instance on every request;
  • optionally dispatch some actions;
  • pull the state out of the store;
  • and then pass the state along to the client.

On the client side, a new Redux store will be created and initialized with the state provided by the server. Redux’s only job on the server side is to provide the initial state of our app.

This might seem not very clear, but the critical part is:

  1. Initialize and create a new redux store for new user request
  2. (Optional) populate the store with information; for example, you could use the user cookies in the request to identify the user and populate the store with the user information.
  3. Send the redux state to the client
  4. The client then uses the received state to initialize the client-side redux store.

The next part will cover how we can achieve this.

Setting up Redux

To get started with Redux, we will create a basic Redux app where we keep track of the counter in our state.

We need to first install redux and react-redux:

npm install --save redux react-redux

This is how our project structure will look like:

hello-next
    +- .next
    +- node_modules
    +- pages
    +- redux
        +- actions
        +- reducers

To do so we can do the following:

mkdir redux redux/actions redux/reducers

We will now create a counterReducer, which will keep track of our counter state. We can place this in the reducers folder:

touch redux/reducers/counterReducer.js

This is how the counterReducer file will look like:

const counterReducer = (state = {value: 0}, action) => {
    return {...state};
};

export default counterReducer;

This will create an initial state with the counter value being set to 0

Right now our counterReducer does nothing. We can proceed to create actions:

touch redux/actions/counterActions.js

We will just specify two actions – Increment and Decrement:

//Action Types
export const INCREMENT_COUNTER = "INCREMENT_COUNTER";
export const DECREMENT_COUNTER = "DECREMENT_COUNTER";


//Action Creator
export const incrementCounter = () => ({
   type: INCREMENT_COUNTER
});

export const decrementCounter = () => ({
    type: DECREMENT_COUNTER
});

We can now modify our reducer to include these actions:

import {DECREMENT_COUNTER, INCREMENT_COUNTER} from '../actions/counterActions';

const counterReducer = (state = {value: 0}, action) => {
    switch (action.type) {
        case INCREMENT_COUNTER:
            return {...state, value: state.value + 1};
        case DECREMENT_COUNTER:
            return {...state, value: state.value - 1};
        default:
            return {...state};
    }
};

export default counterReducer;

This will either increment or decrement our counter when INCREMENT_COUNTER or DECREMENT_COUNTER actions are dispatched.

We can now proceed to create the root reducer, which will be responsible for combining all our reducers. In our case we only have 1 reducer “counterReducer”, however for common practice, we will proceed to combine reducers.

Create the rootReducer file:

touch redux/reducers/rootReducer.js

This is how our rootReducer file will look like:

import counterReducer from './counterReducer';
import {combineReducers} from 'redux';

const rootReducer = combineReducers({
    counter: counterReducer
});

export default rootReducer;

This combines all our reducers into one rootReducer which we can use to initialise our redux store.

We can now proceed to create our redux store:

touch redux/store.js
import {createStore} from 'redux';
import rootReducer from './reducers/rootReducer';

const store = createStore(rootReducer);

export default store;

Now that we have our redux logic setup, we can link our application with redux, using react-redux. However, to do this, we need to create a special file named “_app.jsx” located in our pages directory:

touch pages/_app.jsx

next uses the App component to initialize pages. We created the “_app. jsx” file to override the default App Component. To begin with, our new App Component needs to extend the default App component, so that next can still use it to initialize pages.

We can import the default App Component from “next/app” and create our own App component:

import App from 'next/app';

class MyApp extends App {


}

export default MyApp;

However, at this moment we are doing nothing. Similar to how Redux is connected to Client-side react apps, we can connect our server-side rendered application here.

We use “Provider” provided by react-redux and connect our store:

import App from 'next/app';
import {Provider} from 'react-redux';
import React from 'react';

class MyApp extends App {

    render() {
        return (
            <Provider store={}>

            </Provider>
        );
    }

}

export default MyApp;

But what do we put as the argument for store inside the Provider Component? To finish the setup we must use a static function getInitialProps. This function according to the next docs is responsible for:

Next.js comes with getInitialProps, which is an async function that can be added to any page as a static method.

getInitialPropsallows the page to wait for data before rendering starts.

Using getInitialProps will make the page opt-in to on-demand server-side rendering.

Every page that has getInitialProps will be server-side rendered. If you do not include this method then the file will be rendered to static HTML at next build time. Including this function will allow this page to render on the server, and everything inside that function will be executed before sending the page to the client. This is helpful in cases where our page needs data that needs to be fetched. Returning anything from this function will allow that information to be sent to the client. The client can access the information returned from this function using the props of the React component.

This is also where we can choose to optionally populate our redux state before sending it to the client, adding this function to our “_app.jsx” looks like this:

import App from 'next/app';
import {Provider} from 'react-redux';
import React from 'react';
import {INCREMENT_COUNTER} from '../redux/actions/counterActions';

class MyApp extends App {

    static async getInitialProps({Component, ctx}) {
        const pageProps = Component.getInitialProps ? await Component.getInitialProps(ctx) : {};

        //Anything returned here can be access by the client
        return {pageProps: pageProps};
    }

    render() {
        //Information that was returned  from 'getInitialProps' are stored in the props i.e. pageProps
        const {Component, pageProps} = this.props;

        return (
            <Provider store={}>
                <Component {...pageProps}/>
            </Provider>
        );
    }

}

export default MyApp;

ctx is a getInitialProps parameter referring to Context. You can read more about it here

Using getInitialProps in _app.jsx has a different interface. When using it on normal pages, getInitialProps only has 1 parameter ctx. However in our case, since we are overriding the default App Component, we have access to the default App Component. We need to make sure if the default App component makes use of getInitialProps then we need to send whatever that function returned to the client.

Moving on, to pass the store to the client, we need to wrap the original component with React-Redux’s Provider. To make all of this work, we need to install one last library: next-redux-wrapper

npm install --save next-redux-wrapper

Next-redux-wrapper will enable us to create a store at every new request and it will pass it to MyApp (Our App Implementation) as props.

We need to make use of Next-redux-wrapper’s withRedux wrapper and wrap our App component with it.

The withRedux function accepts makeStore as first argument. The makeStore function will receive initial state and should return a new instance of Redux store each time when called, no memoization needed here, it is automatically done inside the wrapper.

After connecting with next-redux-wrapper:

import App from 'next/app';
import {Provider} from 'react-redux';
import React from 'react';
import withRedux from "next-redux-wrapper";
import store from '../redux/store';

class MyApp extends App {

    static async getInitialProps({Component, ctx}) {
        const pageProps = Component.getInitialProps ? await Component.getInitialProps(ctx) : {};

        //Anything returned here can be accessed by the client
        return {pageProps: pageProps};
    }

    render() {
        //pageProps that were returned  from 'getInitialProps' are stored in the props i.e. pageprops
        const {Component, pageProps, store} = this.props;

        return (
            <Provider store={store}>
                <Component {...pageProps}/>
            </Provider>
        );
    }
}

//makeStore function that returns a new store for every request
const makeStore = () => store;

//withRedux wrapper that passes the store to the App Component
export default withRedux(makeStore)(MyApp);


After the following changes, our app is ready! We can now use Redux as we normally would. Changing our index.jsx to incorporate redux.

import React from 'react';
import {connect} from 'react-redux';
import {decrementCounter, incrementCounter} from '../redux/actions/counterActions';

class App extends React.Component {

        static getInitialProps({store}) {}

    constructor(props) {
        super(props);
    }

    render() {
        return (
            <div>
                <button onClick={this.props.incrementCounter}>Increment</button>
                <button onClick={this.props.decrementCounter}>Decrement</button>
                <h1>{this.props.counter}</h1>
            </div>
        );
    }
}

const mapStateToProps = state => ({
    counter: state.counter.value
});

const mapDispatchToProps = {
    incrementCounter: incrementCounter,
    decrementCounter: decrementCounter,
};

export default connect(mapStateToProps, mapDispatchToProps)(App);

We use React-Redux connect to connect the Redux state to our page, and we use mapStateToProps and mapDispatchToProps to connect our state and actionCreators to our page.

After running the page, our React-Redux app works as expected! Clicking on the buttons Increments and/or Decrements!

Congratulations, you now know the basics of how to create a Server-side Rendered React-Redux Application using next.js

One thing to note is that at the moment Redux only works as a Single Page Application, what this means is that Client-side routing is the only way for the Redux store to be transferred between pages.

This means that if the user navigates to a different URL (i.e. Server-side routing) then the server will treat it as a new client and serve an empty redux state. To learn how to persist the redux-state so that the counter values stay the same for each refresh refer to the next-redux-wrapper guide. However please ensure that you update your Next.js version and the next-redux-wrapper version first and follow the updated guide.