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
- Understanding of Basic Redux Concepts
For the purpose of this post, after going through all the concepts, we will be creating a simple counter app with our server-side rendered app.
Getting Started with Next.JS
Next.js is a React-Framework which makes it really 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 next and there is a lot of overlap with the documentation. I recommend going through the documentation and then proceeding to the next part of this article. Nevertheless, if the documentation are not sufficient, then 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 need to 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 named index.jsx
will be mapped to the root url i.e. localhost:3000/
.
Similarily a file named login.jsx
will be mapped to localhost:3000/login
This feature is enabled by default and for our use-case, this is sufficient.
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 create 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;
Doing so will show this result:
Clicking on the appropriate buttons increment and decrements.
As you can see, next
makes use of React and therefore working with next is simple just working with React, the only difference is that next
automatically (Behind-the-scenes) renders the application serverside.
Understanding Server-Side Rendering with Redux
Similar to how working with Next
is basically just working with React. Redux web apps behave in the same way. Everything works similar to how it would work if the app was rendered on the client-side. The only challenge working with Redux is the initial Redux setup with server-side rendering and this is exactly what the following part covers.
The official documentation, Redux provides a good explanation of 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 along in our response, so 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 to also 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 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 confusing but the important part is:
- Initialize and create a new redux store for new user request
- (Optional) populate the store with information, for example, you could make use of the user-cookies in the request to identify the user and populate the store with the user information.
- Send the redux state to the client
- 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 creating 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 creating 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 proceeding to combining 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 initalise our redux store.
We can now proceed to creating 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 initalise 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 anasync
function that can be added to any page as astatic method
.getInitialProps
allows 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:
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 acceptsmakeStore
as first argument. ThemakeStore
function will receive initial state and should return a new instance of Reduxstore
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 the 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's 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 stays the same for each refresh refer to 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.
The code for this project can be found on Github
This is the end of this post! This was my first post and I hope you enjoyed reading it! Any feedback is appreciated!
If you would like to read more yourself refer to next-redux-wrapper repository
Top comments (14)
Please check out our new official Redux Toolkit package. It includes utilities to simplify several common Redux use cases, including store setup, defining reducers, immutable update logic, and even creating entire "slices" of state at once:
redux-toolkit.js.org
Can you plesae show the example how to dispatch actions in getStaticProps and how to get data from Redux into the getStaticPaths , in case if you use dynamic route [id].jsx
/!\ You are using legacy implementaion. Please update your code: use createWrapper() and wrapper.withRedux().
I had the same problem. I find a solution like this: "If you use Thunks middleware you don't need to use next-redux-wrapper." I removed it and my problem was solved.
How does that work @metin1? Can you please explain or give an example? I'm facing the same issue
Thanks. But don't forget to keep versions of packages from repository. In latest versions store isn't taken from "this.props" in MyApp component in _app.jsx
Can't wait for part 2
Excellent content !. Waiting for the next part <3
Could you please tell me how to connect this app to the express server ??
There is no express server with next.js
Is it really good to use Redux on a Next.js app or the other middleware, or not?
Great write up, has got me started in the right direction. Thank you
Hi amazing content. Pleasae the second party regarding localstorage with next js