Next Js React Redux Authentication Tutorial


Posted 4 months ago by Ryan Dhungel

Category: React Hooks Next JS SSR Node Redux React

Viewed 3229 times

Estimated read time: 26 minutes

Course of the day!

React Redux Firebase CRUD App with Authentication

React Redux Firebase CRUD App with Authentication

In this tutorial, you will learn how to properly setup Next Js frontend so that you have flexible authentication system to protect certain routes. We will also be using redux to manage state.

You should have prior understanding of at least the basics of react redux nextjs and nodejs to follow along with this tutorial.

Project setup

mkdir react-redux-next
cd react-redux-next
npm init -y
npm i react react-dom next next-cookie axios cookie-parser express js-cookie next-redux-wrapper react-redux redux redux-thunk
mkdir pages
touch pages/index.js
touch server.js

Update your package.json so that we can run in dev, build and start in production as well.

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

Next step is to create server.js in the root of your project. Here we will use express to create a server. Then pass all incoming requests (using wildcard *) to next for further handling.

// server.js
const express = require('express');
const next = require('next');
const cookieParser = require('cookie-parser');

const port = parseInt(process.env.PORT, 10) || 3000;
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);
  });

Create index.js inside pages folder. It is a requirement of nextjs.

  // pages/index
  const Index = () => (
    <div>
        <h2 className="title is-2">Authentication with Next.js and JWT</h2>
        <p>A proof of concept app, demonstrating the authentication of Next.js application using JWT.</p>
    </div>
);

export default Index;

Now you can run npm run dev in your terminal and see you app in localhost:3000

Setting up redux with next js

Next.js uses the App component to initialize pages. You can override it to control what your pages will receive as props during initialization. You can do so using getInitialProps method in your pages. Lets create _app.js inside pages folder so that we can override the default App component of next js. We are doing this to be able to pass props(properties) from redux store to other pages. This way pages will be able to access redux store as props.

What is getInitialProps in next js?

getInitialProps is one of the core feature of next js. This method runs on both client side and server side. On first initial loading of page, getInitialProps runs in server side. Then onwards it runs on client side.

There are differences on what params you can destructure in getInitialProps method inside pages and _app.

While inside pages you can destructure pathname(path of url), query(query string of url), asPath(string of url path including query), req(server side), res(server side), jsonPageRes(client side for fetch response object) and error in getInitialProps({pathname, query, asPath, req, res, jsonPageRes, err})

While inside _app you can destructure Component, router and ctx(context) in getInitialProps({ Component, router, ctx })

Create a redux store and pass props to Component. Each pages will have access to redux store as props now.

// _app.js
import React from 'react';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import App, { Container } from 'next/app';
import withRedux from 'next-redux-wrapper';

const reducer = (state = { foo: '' }, action) => {
    switch (action.type) {
        case 'FOO':
            return { ...state, foo: action.payload };
        default:
            return state;
    }
};

const makeStore = (initialState, options) => {
    return createStore(reducer, initialState);
};

class MyApp extends App {
    static async getInitialProps({ Component, ctx }) {
        // we can dispatch from here too
        ctx.store.dispatch({ type: 'FOO', payload: 'foo' });
        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(makeStore)(MyApp);

Now pages can access store as props

// pages/index.js
import React, { Component } from 'react';
import { connect } from 'react-redux';

class Index extends Component {
    static getInitialProps({ store, isServer, pathname, query }) {
        // component will be able to read from store's state when rendered
        store.dispatch({ type: 'FOO', payload: 'foo' });
        // you can pass some custom props to component from here
        return { custom: 'custom' };
    }
    render() {
        return (
            <div>
                <div>Prop from Redux {this.props.foo}</div>
                <div>Prop from getInitialProps {this.props.custom}</div>
            </div>
        );
    }
}

export default connect(state => state)(Index);

Try visiting localhost:3000 and see the props from store as well as custom props.

Code refactor using react hooks

const MyApp = ({ Component, pageProps, store }) => {
    return (
        <Container>
            <Provider store={store}>
                <Component {...pageProps} />
            </Provider>
        </Container>
    );
};

MyApp.getInitialProps = async ({ Component, ctx }) => {
    // we can dispatch from here too
    ctx.store.dispatch({ type: 'FOO', payload: 'foo' });
    const pageProps = Component.getInitialProps ? await Component.getInitialProps(ctx) : {};
    return { pageProps };
};

export default withRedux(makeStore)(MyApp);

Proper folder structure for redux

At the moment we have redux completly in _app.js. Lets create separate folders for reducers actions etc

create a folder called redux in the root of your project.

// redux/index.js
import { createStore } from 'redux';

export const reducer = (state = { foo: '' }, action) => {
    switch (action.type) {
        case 'FOO':
            return { ...state, foo: action.payload };
        default:
            return state;
    }
};

export const makeStore = (initialState, options) => {
    return createStore(reducer, initialState);
};

Now import it in _app.js and use

// remove
import { createStore } from 'redux';

// import
import { makeStore } from '../redux';

Your app should continue to work as before. Lets further extract reducers to its own folder so that we can import in redux/index.js and use.

// redux/reducers/fooReducer.js
const reducer = (state = { foo: '' }, action) => {
    switch (action.type) {
        case 'FOO':
            return { ...state, foo: action.payload };
        default:
            return state;
    }
};

export default reducer;

Now lets import fooReducer in redux/index.js

// redux/index.js
import { createStore } from 'redux';
import reducer from './reducers/fooReducer';

export const makeStore = (initialState, options) => {
    return createStore(reducer, initialState);
};

Our app continues to work as before. Instead of requiring individual reducers into redux/index.js lets create a file called index.js inside reducers folder. There we will import combineReducers method from redux that allows us to combine multiple reducers. Lets do it so that we can export rootReducer form reducers/index.js and can easily import in redux/index.js to use.

// redux/reducers/index.js
import { combineReducers } from 'redux';
import fooReducer from './fooReducer';

const rootReducer = combineReducers({
    foo: fooReducer
});

export default rootReducer;

Now import rootReducer in redux/index.js to repace the individual fooReducer that we imported earlier

// redux/index.js
// replace this
import reducer from './reducers/fooReducer';
// with this
import reducer from './reducers';

If you are getting error Objects are not valid as a React child (found: object with keys {foo}) you need to do the following in your pages/index.js

// pages/index.js
<div>Prop from Redux {JSON.stringify(this.props.foo)}</div>

Using redux thunk and redux devtools as middlewares in redux store

// redux/index.js
import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import thunk from 'redux-thunk';
import reducer from './reducers';

export const makeStore = (initialState, options) => {
    return createStore(reducer, initialState, composeWithDevTools(applyMiddleware(thunk)));
};

Using redux actions

// actions/fooActions.js
import axios from 'axios';

export const getPosts = () => dispatch =>
  axios({
    method: 'GET',
    url: `https://jsonplaceholder.typicode.com/posts`,
    headers: []
  }).then(response => dispatch({ type: 'FOO', payload: response.data }));

Now instead of passing string 'foo' when we dispatch 'FOO' lets dispatch getPosts which will dispatch action type 'FOO' with payload of posts from the API.

We get list of posts from API when the page loads. We also have onclick button that fires getPosts which is available as props from store (thanks to connect method)

// pages/index.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { getPosts } from '../redux/actions/fooActions';
import axios from 'axios';

const Index = props => {
    const handleSubmit = e => {
        e.preventDefault();
        props.getPosts();
    };
    return (
        <div>
            <div>Prop from Redux {JSON.stringify(props)}</div>
            <button onClick={handleSubmit}>Load</button>
            <div>Prop from getInitialProps {props.custom}</div>
        </div>
    );
};

Index.getInitialProps = async ({ store, isServer, pathname, query }) => {
    await store.dispatch(getPosts());
    return { custom: 'custom' };
};

export default connect(
    state => state,
    { getPosts }
)(Index);

Now if you visit page you will see all the post from API.

Using redux action types
// redux/actionTypes.js
export const FOO = 'FOO';

Now import this to replace hard coded 'FOO' string in fooReducer.js and fooActions.js

// fooReducer.js
import { FOO } from '../actionTypes';

// fooActions.js
import { FOO } from '../actionTypes';

React Redux NextJs Authentication

Lets begin by creating Layout component where we will have a navigation. It will be used across all pages

import Link from 'next/link';
import Head from 'next/head';

const Layout = ({ children, title }) => (
  <div>
    <Head>
      <title>{title}</title>
    </Head>
    <div>
      <ul>
        <li>
          <Link href="/">
            <a>Home</a>
          </Link>
        </li>
        <li>
          <Link href="/signin">
            <a>Sign In</a>
          </Link>
        </li>
        <li>
          <Link href="/signup">
            <a>Sign Up</a>
          </Link>
        </li>
        <li>
          <Link href="/whoami">
            <a>Who Am I</a>
          </Link>
        </li>
      </ul>
    </div>

    <div className="has-text-centered">{children}</div>
  </div>
);

export default Layout;

Now use Layout in pages/index.js

// pages/index.js
import Layout from '../components/Layout';

const Index = ({ foo, custom }) => {
    return (
        <Layout>
            <div>
                // rest of code
            </div>
            <div>Prop from getInitialProps {custom}</div>
        </Layout>
    );
};

Lets create signin page

import React, { useState, useEffect } from 'react';
import { connect } from 'react-redux';
import Layout from '../components/Layout';

const Signin = () => {
    const [email, setEmail] = useState('[email protected]');
    const [password, setPassword] = useState('rrrrrr9');

    const handleSubmit = e => {
        e.preventDefault();
        console.log('login with ', { email, password });
    };

    return (
        <Layout title="Sign In">
            <h3>Sign In</h3>
            <form onSubmit={handleSubmit}>
                <div>
                    <input
                        type="email"
                        placeholder="Email"
                        required
                        value={email}
                        onChange={e => setEmail(e.target.value)}
                    />
                </div>
                <div>
                    <input
                        className="input"
                        type="password"
                        placeholder="Password"
                        required
                        value={password}
                        onChange={e => setPassword(e.target.value)}
                    />
                </div>
                <div>
                    <button type="submit">Sign In</button>
                </div>
            </form>
        </Layout>
    );
};

export default Signin;

Connect signin page with redux store

First lets create action type then auth actions and auth reducer

Then connect signin page to redux store

// actionTypes.js
export const AUTHENTICATE = 'AUTHENTICATE';

// actions/authActions.js
import { AUTHENTICATE } from '../actionTypes';

export const authenticate = user => dispatch =>
    fetch(`http://localhost:8000/api/signin`, {
        method: 'POST',
        headers: {
            Accept: 'application/json',
            'Content-Type': 'application/json'
        },
        body: JSON.stringify(user)
    })
        .then(response => dispatch({ type: AUTHENTICATE, payload: response.data.token }))
        .catch(err => console.log(err));

// reducers/authReducers.js
import { AUTHENTICATE } from '../actionTypes';

const authReducer = (state = { token: null }, action) => {
    switch (action.type) {
        case AUTHENTICATE:
            return { ...state, token: action.payload };
        default:
            return state;
    }
};

export default authReducer;

// bring in reducers to reducers/index.js
import { combineReducers } from 'redux';
import fooReducer from './fooReducer';
import authReducer from './authReducer';

const rootReducer = combineReducers({
    foo: fooReducer,
    authentication: authReducer
});

export default rootReducer;

Now in signin page, we can fill the email and password to signin. This will send request to backend on the url localhost:8000/api/signin. If you dont have your own backend API built with node js, you can download my API from github and follow along.

Download my Node API from Github. Then run npm install, create .env file and add variables for MONGO_URI and JWT_SECRET. Then you can run npm start to start up the API.

// pages/signin.js
import React, { useState, useEffect } from 'react';
import { connect } from 'react-redux';
import { authenticate } from '../redux/actions/authActions';
import Layout from '../components/Layout';

const Signin = ({ authenticate }) => {
    const [email, setEmail] = useState('[email protected]');
    const [password, setPassword] = useState('rrrrrr9');

    const handleSubmit = e => {
        e.preventDefault();
        // console.log('login with ', { email, password });
        const user = { email, password };
        authenticate(user);
    };

    return (
        <Layout title="Sign In">
            <h3>Sign In</h3>
            <form onSubmit={handleSubmit}>
                <div>
                    <input
                        type="email"
                        placeholder="Email"
                        required
                        value={email}
                        onChange={e => setEmail(e.target.value)}
                    />
                </div>
                <div>
                    <input
                        className="input"
                        type="password"
                        placeholder="Password"
                        required
                        value={password}
                        onChange={e => setPassword(e.target.value)}
                    />
                </div>
                <div>
                    <button type="submit">Sign In</button>
                </div>
            </form>
        </Layout>
    );
};

Signin.getInitialProps = ctx => {};

export default connect(
    state => state,
    { authenticate }
)(Signin);

Now if you try signin, you will get token back from the API. Open browser console > Network and click on signin name on the left sidebar to see the token received from backend. On right main window click Headers to see what was sent as headers and click Response to see the response you got from API.

Save the JWT in browser cookie and perform authentication check 

Now we need to store this token in browser cookie. Save JWT in browser cookie and check if next has cookie in ctx.req.headers.cookie. We can perform this check in getInitialProps.

First we need to create few helper methods to save, remove and get cookies. We need to get cookie in both client side and server side. Here is the full code for actions/authActions.js

// actions/authActions.js
import cookie from 'js-cookie';
import { AUTHENTICATE } from '../actionTypes';

export const authenticate = user => dispatch =>
    fetch(`http://localhost:8000/api/signin`, {
        method: 'POST',
        headers: {
            Accept: 'application/json',
            'Content-Type': 'application/json'
        },
        body: JSON.stringify(user)
    })
        .then(response => dispatch({ type: AUTHENTICATE, payload: response.data.token }))
        .catch(err => console.log(err));

/**
 * cookie helper methods
 */

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];
};

Now lets save token in browser cookie. Modify your authenticate method

export const authenticate = user => dispatch =>
    fetch(`http://localhost:8000/api/signin`, {
        method: 'POST',
        headers: {
            Accept: 'application/json',
            'Content-Type': 'application/json'
        },
        body: JSON.stringify(user)
    })
        .then(data => data.json())
        .then(response => {
            // console.log('ok set cookie', response.token);
            setCookie('token', response.token);
            Router.push('/');
            dispatch({ type: AUTHENTICATE, payload: response.token });
        })
        .catch(err => console.log(err));

Save the token in the redux store and remove on logout

First add actionTypes, then authActions and authReducers

// actionTypes.js
export const DEAUTHENTICATE = 'DEAUTHENTICATE';

// actions/authActions.js
import { AUTHENTICATE, DEAUTHENTICATE } from '../actionTypes';

// gets the token from the cookie and saves it in the store
export const reauthenticate = token => {
    return dispatch => {
        dispatch({ type: AUTHENTICATE, payload: token });
    };
};

// removing the token
export const deauthenticate = () => {
    return dispatch => {
        removeCookie('token');
        Router.push('/');
        dispatch({ type: DEAUTHENTICATE });
    };
};

// reducers/authReducer.js
import { AUTHENTICATE, DEAUTHENTICATE } from '../actionTypes';

const authReducer = (state = { token: null }, action) => {
    switch (action.type) {
        case AUTHENTICATE:
            return { ...state, token: action.payload };
        case DEAUTHENTICATE:
            return { token: null };
        default:
            return state;
    }
};

export default authReducer;

Now we can try deauthenticating user. First lets connect our Layout component to redux then we can implement signout.

// Layout.js
import Link from 'next/link';
import Head from 'next/head';
import { connect } from 'react-redux';
import { deauthenticate } from '../redux/actions/authActions';

const Layout = ({ children, title, deauthenticate, isAuthenticated }) => (
  <div>
    <Head>
      <title>{title}</title>
    </Head>
    <div>
      <ul>
        <li>
          <Link href="/">
            <a>Home</a>
          </Link>
        </li>
        {!isAuthenticated && (
          <li>
            <Link href="/signin">
              <a>Sign In</a>
            </Link>
          </li>
        )}
        {!isAuthenticated && (
          <li>
            <Link href="/signup">
              <a>Sign Up</a>
            </Link>
          </li>
        )}

        {isAuthenticated && (
          <li onClick={deauthenticate}>
            <a>Sign Out</a>
          </li>
        )}

        <li>
          <Link href="/whoami">
            <a>Who Am I</a>
          </Link>
        </li>
      </ul>
    </div>

    <div className="has-text-centered">{children}</div>
  </div>
);

const mapStateToProps = state => ({ isAuthenticated: !!state.authentication.token });

export default connect(
  mapStateToProps,
  { deauthenticate }
)(Layout);

Now you can signin. Token will be saved in the browser cookie while in the client side. During server side it will be in the req.headers.cookie. If authenticated then we can deauthenticate user by removing the token from the cookie. We have also conditionally show hide signin signout buttons.

Protected routes

Lets create Whoami page. It will be protected for authenticated users only. We will show user info by making request to bakend. Lets continue.

Show user info in Whoami. Moving ahead we will make a request to backend to fetch that particular user info as well.

// Whoami.js
import axios from 'axios';
import { connect } from 'react-redux';
import Layout from '../components/Layout';

const Whoami = ({ user }) => (
  <Layout title="Who Am I">
    <h2>Who am i</h2>
    {JSON.stringify(user)}
  </Layout>
);

Whoami.getInitialProps = async ctx => {
  const token = ctx.store.getState().authentication.token;
  if (token) {
    return {
      user : "Ryan"
    };
  }
};

export default connect(state => state)(Whoami);

Preserve user logged in state in server side rendering (SSR)

At the moment our login functionality works. We are saving token in the cookie as well. But as soon as you refresh the page, we loose it. Because its stored in the client side cookie. We need to checks if the page is being loaded on the server, and if so, get auth token from the cookie:

// Whoami.js
import axios from 'axios';
import { connect } from 'react-redux';
import { reauthenticate, getCookie } from '../redux/actions/authActions';
import Router from 'next/router';
import Layout from '../components/Layout';

const Whoami = ({ user }) => (
  <Layout title="Who Am I">
    {(user && (
      <div>
        <h2>Who am i</h2>
        {JSON.stringify(user)}
      </div>
    )) ||
      'Please sign in'}
  </Layout>
);

Whoami.getInitialProps = async ctx => {
  if (ctx.isServer) {
    if (ctx.req.headers.cookie) {
      const token = getCookie('token', ctx.req);
      console.log('WHOAMI ', token);
      ctx.store.dispatch(reauthenticate(token));
    }
  } else {
    const token = ctx.store.getState().authentication.token;

    if (token && (ctx.pathname === '/signin' || ctx.pathname === '/signup')) {
      setTimeout(function() {
        Router.push('/');
      }, 0);
    }
  }

  const token = ctx.store.getState().authentication.token;
  if (token) {
    return {
      user: 'Ryan'
    };
  }
};

export default connect(
  state => state,
  { reauthenticate }
)(Whoami);

Now login and go to '/whoami' and refresh the page. The user data is there. But if you go to home page and refresh the page. The logged in state is lost. So when the page is loaded in the server we need to run the above code in all the pages . Lets move it to authActions.js

// authActions.js
// check if the page is being loaded on the server, and if so, get auth token from the cookie
export const checkServerSideCookie = ctx => {
    if (ctx.isServer) {
        if (ctx.req.headers.cookie) {
            const token = getCookie('token', ctx.req);
            ctx.store.dispatch(reauthenticate(token, user));
        }
    } else {
        const token = ctx.store.getState().authentication.token;

        if (token && user && (ctx.pathname === '/signin' || ctx.pathname === '/signup')) {
            setTimeout(function() {
                Router.push('/');
            }, 0);
        }
    }
};

Now import in whoami and signin and use

// whoami.js and index.js
import { reauthenticate, getCookie, checkServerSideCookie } from '../redux/actions/authActions';

Whoami.getInitialProps = async ctx => {
  checkServerSideCookie(ctx);
  // rest of code
}

Or you can do it in _app.js. So that you dont have to manually import and use in each pages.

Making a request to backend to get user info

// Whoami.js
import axios from 'axios';
import { connect } from 'react-redux';
import { reauthenticate, getCookie, checkServerSideCookie, isAuthenticated } from '../redux/actions/authActions';
import Router from 'next/router';

import Layout from '../components/Layout';

const Whoami = ({ user }) => (
  <Layout title="Who Am I">
    {(user && (
      <div>
        <h2>Who am i</h2>
        {JSON.stringify(user)}
      </div>
    )) ||
      'Please sign in'}
  </Layout>
);

Whoami.getInitialProps = async ctx => {
  checkServerSideCookie(ctx);

  const { token } = ctx.store.getState().authentication;

  // return {
  //   user
  // };

  if (token) {
    const response = await axios.get(`http://localhost:8000/api/user/5cccd8b71bed6f30a4921f48`, {
      headers: {
        authorization: `Bearer ${token}`,
        contentType: 'application/json'
      }
    });
    const user = response.data;
    return {
      user
    };
  }
};

export default connect(
  state => state,
  { reauthenticate }
)(Whoami);

 

This is the end of Next Js React Redux Authentication Tutorial. If you have come across any issues, leave a comment. Cheers!