In this article, we will discuss how we integrate Redux store with our next js app by covering simple JWT auth and we will do all that without compromising on SSR.

Disclaimer: This is strictly a hands-on guide targeted towards developers with an advanced skill set in next js.

Following concepts will be covered.

How to structure domain logic in Redux.

Integrating Redux store with next js.

Rebuilding state on page changes.

Persisting state in cookies without sacrificing SSR.

Setup server-side routing to support cookies.

Lets Code

We start by creating our base directory nextjs-redux.

Within our base directory we will create following files and directories.

packages.json

components

pages

state

utils

server.js

In pages directory, we will create following files

_app.js

_document.js

In the components directory, the file and directory structure will be as follows.

login.js

In state directory, the file and directory structure will be as follows.

actions

reducers

store.js

In actions directory, there will be two files, ActionConstants.js and ActionCreator.js.In reducers directory, we create only one file Reducer.js.

First, we create our first file packages.json.

{ "scripts": { "dev": "node server.js", "build": "next build", "start": "NODE_ENV=production node server.js" } }

After creating packages.json file and adding the following code, we will install our required modules.

npm install --save react react-dom next

npm install --save redux react-redux redux-thunk

This command will install all the required packages to build our next js app and for redux store.



Now we install another package next-redux-wrapper. We use this package to bind or wrap our next js app with redux.

npm install --save next-redux-wrapper

And for chrome redux devtool extension, we will install package redux-devtools-extension.

npm install --save-dev redux-devtools-extension

After all this installation, now we complete our first file ActionConstants.js. Add the following lines of code.

export const AUTHENTICATE = 'AUTHENTICATE'; export const DEAUTHENTICATE = 'DEAUTHENTICATE'; export const RESTORE_AUTH_STATE = "RESTORE_AUTH_STATE";

Now we complete our ActionCreator.js file.

import {RESTORE_AUTH_STATE, AUTHENTICATE, DEAUTHENTICATE} from "./AuthActionConstants"; export const authenticateAction = (user) => { return { type: AUTHENTICATE, payload: user }; }; export const deAuthenticateAction = () => { return { type: DEAUTHENTICATE, }; }; export const restoreState = (authState) => { return { type: RESTORE_AUTH_STATE, payload: authState } }; export const login = loginDetails => { return async dispatch => { try{ dispatch(deAuthenticateAction()); // login code. And storing data in result variable dispatch(authenticateAction(result)); }catch (e) { dispatch(deAuthenticateAction()); } }; }; export const signUp = signUpDetails => { return async dispatch => { try{ dispatch(deAuthenticateAction()); // Signup code. And storing data in result variable dispatch(authenticateAction(result)); }catch (e) { dispatch(deAuthenticateAction()); } }; }; export const logout = () => { return async dispatch => { dispatch(deAuthenticateAction()) } }; export const restore = (savedState) => { return dispatch => { dispatch(restoreState(savedState)); }; };

After completing our actions, we create a cookie.js file in utils directory. Add the following lines of code in the file.

import cookie from 'js-cookie'; export const setCookie = (key, value) => { if (process.browser) { cookie.set(key, value, { expires: 1, path: '/' }); } }; export const removeCookie = (key) => { if (process.browser) { cookie.remove(key, { expires: 1 }); } }; export const getCookie = (key, req) => { return process.browser ? getCookieFromBrowser(key) : getCookieFromServer(key, req); }; const getCookieFromBrowser = key => { return cookie.get(key); }; const getCookieFromServer = (key, req) => { if (!req.headers.cookie) { return undefined; } const rawCookie = req.headers.cookie .split(';') .find(c => c.trim().startsWith(`${key}=`)); if (!rawCookie) { return undefined; } return rawCookie.split('=')[1]; };

Important things to observe here are that we are going to need a mechanism to extract cookies on client and server side separately.

Now, we create our reducer. Add the following line of code in AuthReducer.js.

import {RESTORE_AUTH_STATE, AUTHENTICATE, DEAUTHENTICATE} from "../actions/AuthActionConstants"; import {getCookie, setCookie, removeCookie} from '../../utils/cookie'; let initialState; if (typeof localStorage !== "undefined") { const authCookie = getCookie('auth'); if (authCookie) { initialState = JSON.parse(decodeURIComponent(authCookie)); } else { initialState = { isLoggedIn: false, user: {} } } } else { initialState = { isLoggedIn: false, user: {} }; } const authReducer = (state = initialState, action) => { switch (action.type) { case DEAUTHENTICATE: removeCookie("auth"); return { isLoggedIn: false }; case AUTHENTICATE: const authObj = { isLoggedIn: true, user: action.payload }; setCookie("auth", authObj); return authObj; case RESTORE_AUTH_STATE: return { isLoggedIn: true, user: action.payload.user }; default: return state; } }; export default authReducer;

Here you can notice how we are setting initial state by detecting if a store is being created on server side or client side.

After creating actions and reducers, now we complete our store.js file. And add the following code in store.js file.

import { createStore, applyMiddleware, combineReducers } from 'redux' import { composeWithDevTools } from 'redux-devtools-extension' import thunkMiddleware from 'redux-thunk' import AuthReducer from "./reducers/AuthReducer"; import logger from 'redux-logger' const reducers = combineReducers({auth: AuthReducer}); export const initStore = (initialState = {}) => { return createStore( reducers, initialState, composeWithDevTools(applyMiddleware(thunkMiddleware, logger)) ) };

Notice: We are using thunk here to handle async actions related to auth API's

Now, we create our _app.js file in pages directory. Add the following lines of code.

import React from "react"; import App, { Container } from "next/app"; import { Provider } from "react-redux"; import withRedux from "next-redux-wrapper"; import { initStore } from "../state/store"; class MyApp extends App { static async getInitialProps({ Component, ctx }) { const pageProps = Component.getInitialProps ? await Component.getInitialProps(ctx) : {}; return { pageProps }; } render() { const { Component, pageProps, store } = this.props; return ( <Container> <Provider store={store}> <Component {...pageProps} /> </Provider> </Container> ); } } export default withRedux(initStore)(MyApp);

We used next redux wrapper which internally use high order component to inject redux. It's an excellent plugin I recommend to check it out on github.

After completing all these steps, now we create our signin.js file in pages directory.

import React, {Component} from 'react'; import Login from '../components/register/login'; import {connect} from 'react-redux' import {Router} from '../routes' import {login} from "../state/actions/AuthActionCreators"; class LoginPage extends Component { constructor(props) { super(props); } componentDidUpdate(prevProps, prevState, snapshot) { if(this.props.isLoggedIn === true){ Router.pushRoute('/'); } } handleLoginSubmit = (value) => { const {dispatch} = this.props; dispatch(login(value)); }; render() { return ( <div> <Login onChange={this.handleLoginSubmit}/> </div> ) } } function mapStateToProps(state) { return { user: state.auth.user, isLoggedIn: state.auth.isLoggedIn }; } export default connect(mapStateToProps)(LoginPage);

In above file we covered how to map redux store state to page props and how to redirect user to home if he is not logged in.

After creating signin or login page, we create login.js in components folder. And add the following lines of code.

import React, {Component} from 'react'; class Login extends Component { constructor(props) { super(props); this.state = { phoneNumber: null, password: null, rememberMe: false, }; } handlePhoneNumChange = (e) => { this.setState({ phoneNumber: e.target.value }); }; handlePinCodeChange = (e) => { this.setState({ password: e.target.value }); }; handleRememberMeCheck = (e) => { this.setState({ rememberMe: e.target.checked }); }; handleSubmit = async (event) => { event.preventDefault(); this.props.onChange(this.state); }; render() { return ( <main className="main-content"> <div className="bg-white rounded shadow-7 w-400 mw-100 p-6" > <h5 className="mb-7">Sign into your account</h5> <form id="login" onSubmit={this.handleSubmit} > <div className="form-group"> <input onChange={this.handlePhoneNumChange} type="text" className="form-control" name="phone_number" placeholder="eg. +34645136228" /> </div> <div className="form-group"> <input onChange={this.handlePinCodeChange} type="password" className="form-control" name="customer_pin" placeholder="Enter your pin"/> </div> <div className="form-group flexbox py-3"> <div className=""> <input type="checkbox" onChange={this.handleRememberMeCheck} className="remember"/> <label className="remember">Remember me</label> </div> <a className="text-muted small-2" href="/reset">Forgot password?</a> </div> <div className="form-group"> <button className="btn btn-block btn-primary" type="submit">Login</button> </div> </form> <hr className="w-30"/> <p className="text-center text-muted small-2">Don't have an account? <a href="/register">Register here</a></p> </div> </main> ) } } export default Login;

In the end we create our server.js file. Add the following lines.

const express = require('express'); const next = require('next'); const cookieParser = require('cookie-parser'); const port = parseInt(process.env.PORT, 10) || 4000; const dev = process.env.NODE_ENV !== 'production'; const app = next({ dev }); const handle = app.getRequestHandler(); app.prepare() .then(() => { const server = express(); server.use(cookieParser()); server.get('*', (req, res) => { return handle(req, res); }); server.listen(port, (err) => { if (err) throw err; console.log(`> Ready on http://localhost:${port}`); }); }) .catch((ex) => { console.error(ex.stack); process.exit(1); });

Managing NextJs + Redux Services

After completing all these files and steps. Run a command to start a server.

To run application as a development.

npm run dev

To run application in a production environment.

next build

npm start

After running the next-redux application. You can access the application on localhost port 4000.