Handling user authentication with Redux Toolkit – LogRocket Blog

Alert action creators

In the folder structure above we can see a folder named actions, It contains action creator related to any action inside the app.

Alert reducer (src/reducers/alert.reducer.js)

This reducer manages the needed alert notifications in the application, it updates state when an alert action is dispatched from anywhere in the application.

import { appConstants } from‘../helpers/app-constants’;

 

exportfunctionalert(state = {}, action) {

  switch (action.type) {

    caseappConstants.SUCCESS:

    return {

      type:‘alert-success’,

      message:action.message

    };

    caseappConstants.ERROR:

    return {

      type:‘alert-danger’,

      message:action.message

    };

    caseappConstants.CLEAR:

    return {};

    default:

    returnstate

  }

}

Alert.actions.ts(src/actions/action.action.js)

import { appConstants } from‘../helpers’;

 

exportconstalertActions = {

  success,

  error,

  clear

};

 

functionsuccess(message) {

  return { type:appConstants.SUCCESS, message };

}

 

functionerror(message) {

  return { type:appConstants.ERROR, message };

}

 

functionclear() {

  return { type:appConstants.CLEAR };

}

In the above file, all types of alert types have been defined.

App constants (src/helpers/app-components.js)

exportconstappConstants = {

  LOGIN_REQUEST:‘USERS_LOGIN_REQUEST’,

  LOGIN_SUCCESS:‘USERS_LOGIN_SUCCESS’,

  LOGIN_FAILURE:‘USERS_LOGIN_FAILURE’,

  SUCCESS:‘ALERT_SUCCESS’,

  ERROR:‘ALERT_ERROR’,

  CLEAR:‘ALERT_CLEAR’,

  LOGOUT:‘USERS_LOGOUT’,

  REGISTER_REQUEST:‘USERS_REGISTER_REQUEST’,

  REGISTER_SUCCESS:‘USERS_REGISTER_SUCCESS’,

  REGISTER_FAILURE:‘USERS_REGISTER_FAILURE’

};

Benefits of redux

Since all data flow is strictly one-way, and because data is never mutated, Redux makes it possible to track every single state change in an application. This has some cool implications, including the ability to easily undo and redo data changes and to track actions so that errors can be logged.

Configuring the redux store

Redux Toolkit introduces a new way of creating a store. It separates parts of the store into different files, known as slices.

Create react app

Create a new react app using below command on terminal

Creating the redux store

In Flux, many stores are used within the app, but with Redux, there is only one. A Redux store holds the application’s state and lets us use the dispatch function to call our actions. In the case of our React app, we can provide the single store to the top-level component.

import React from'react'import{ render }from'react-dom'import{ createStore, applyMiddleware }from'redux'import{ Provider }from'react-redux'import App from'./containers/App'import quotesApp from'./reducers'import thunkMiddleware from'redux-thunk'import api from'./middleware/api'let createStoreWithMiddleware =applyMiddleware(thunkMiddleware, api)(createStore)let store =createStoreWithMiddleware(quotesApp)let rootElement = document.getElementById('root')render(<Providerstore={store}><App/></Provider>,
  rootElement
)

Note here that we are applying middleware as we create our store. This is looking ahead and we’ll describe what the thunkMiddleware and api middleware do later on. We are importing the quotesApp reducer, which we’ve yet to create, and this is used to create the store. Before creating the reducer, let’s create the App container component.

Dotenv

Path: /.env

Example next.js api

The Next.js API contains two routes/endpoints:

Example next.js client app

The Next.js client (React) app contains two pages:

Export actions and reducer for redux slice

The authActions export includes all sync actions (slice.actions) and async actions (extraActions) for the auth slice.

The reducer for the auth slice is exported as authReducer, which is used in the Redux store for the app to configure the global state store.

Fetch wrapper

Path: /helpers/fetch-wrapper.js

Fetching quotes with api middleware

Redux lets us tie in middleware to our apps, which opens up a lot of possibilities. With it, we can easily do things like logging. Another common use of middleware is for setting up API communication. Let’s create a middleware that calls our API for quotes. Our setup is well-informed by the API middleware from Redux’s real world example.

Form validator (src/helpers/form-validator.js)

importvalidatorfrom‘validator’;

 

classFormValidator {

constructor(validations) {

this.validations = validations;

}

 

validate(state) {

letvalidation = this.valid();

 

// for each validation rule

this.validations.forEach(rule=> {

 

if (!validation[rule.field].isInvalid) {

 

constfield_value = state[rule.field].toString();

constargs = rule.args || [];

constvalidation_method =

typeofrule.method === ‘string’ ?

validator[rule.method] :

rule.method

 

if (validation_method(field_value, …args, state) !== rule.validWhen) {

validation[rule.field] = {

isInvalid:true,

message:rule.message

}

validation.isValid = false;

}

}

});

 

returnvalidation;

}

 

valid() {

constvalidation = {}

 

this.validations.map(rule=> (

validation[rule.field] = {

isInvalid:false,

message:

}

));

 

return {

isValid:true,

validation

};

}

}

 

exportdefaultFormValidator;

Global css styles

Path: /styles/globals.css

The globals.css file contains global custom CSS styles for the example JWT auth app. It’s imported into the application by the Next.js app component.

a { cursor: pointer; }

.app-container {
    min-height: 350px;
}

Handling asynchronous functions in extrareducers

Actions created with createAsyncThunk generate three possible lifecycle action types: pending, fulfilled, and rejected.

Helpers

In the project, we have used some functions/codes as a helper.

Home component

Path: /src/home/Home.jsx

Home page

Path: /pages/index.jsx

Index.js (src/actions/index.js)

The above file is used just for easy access of action files in the application.

Javascript config

Path: /jsconfig.json

Jsconfig.json

Path: /jsconfig.json

The below configuration enables support for absolute imports to the application, so modules can be imported with absolute paths instead of relative paths (e.g. import { MyComponent } from ‘_components’; instead of import { MyComponent } from ‘../../../_components’;).

Jwt authentication in a react-redux app

Are you currently working on JWT authentication in React and Redux App? Don’t you know how to handle it? In this article we will cover a sign in process step by step.

Link component

Path: /components/Link.jsx

A custom link component that wraps the Next.js link component to make it work more like the standard link component from React Router.

Login component

Login.js (src/components/auth/Login.js)

importReact, { Component } from‘react’;

import {  Link } from‘react-router-dom’;

import { connect } from‘react-redux’;

import { userActions } from‘../../actions’;

 

classLoginextendsComponent {

  constructor(props) {

    super(props);

    this.props.logout();

    this.state = {

      email:,

      password:,

      submitted:false

    };

    this.handleInputChange = this.handleInputChange.bind(this);

    this.submitLogin = this.submitLogin.bind(this);

  }

 

  handleInputChange(e) {

    letname = e.target.name;

    letvalue = e.target.value;

    this.setState({

      [name]:value

    });

  }

 

  submitLogin(e) {

    e.preventDefault();

    this.setState({ submitted:true });

    const { email, password } = this.state;

    if (email && password) {

      this.props.login(email, password);

    }

  }

 

  render(){

    const { loggingIn } = this.props;

    const { email, password, submitted } = this.state;

    return(

      <divclassName=“col-md-4 col-md-offset-4”>

        <h2className=“text-center”>User Login</h2>

          <formname=“form”>

            <divclassName={‘form-group’ (submitted && !email ? ‘ has-error’ : )}>

              <labelfor=“email”>Email:</label>

              <inputtype=“text”id=“email”className=“form-control input-shadow”placeholder=“Enter Email”value={this.state.email}onChange={this.handleInputChange}name=“email”/>

              {submitted && !email && <divclassName=“help-block”>Email is required</div>}

            </div>

            <divclassName={‘form-group’ (submitted && !password ? ‘ has-error’ : )}>

              <label>Password: </label>

              <inputtype=“password”id=“exampleInputPassword”className=“form-control input-shadow”placeholder=“Enter Password”value={this.state.password} onChange={this.handleInputChange} name=“password”/>

              {submitted && !email && <divclassName=“help-block”>Password is required</div>}

            </div>

            <buttontype=“button”onClick={this.submitLogin}className=“btn btn-primary btn-block”>Sign In</button>

            <Linkto=“/register”className=“btn btn-link”>Register</Link>

          </form>

       </div>

    )

  }

}

functionmapState(state) {

  const { loggingIn } = state.authentication;

  return { loggingIn };

}

 

constactionCreators = {

  login:userActions.login,

  logout:userActions.logout

};

 

constconnectedLoginPage = connect(mapState, actionCreators)(Login);

export { connectedLoginPageasLogin };

In the above file(Login Component) renders a simple login form with fields, email, password, and a button to submit the form. The available fields are also defined in state and updating on change of values in it with the help of handleChange function. On submit of the form button submitLogin() method is called which is defined at the top.

Login page

Path: /pages/login.jsx

Main index html file

Path: /public/index.html

The main index.html file is the initial page loaded by the browser that kicks everything off. Create React App (with Webpack under the hood) bundles all of the compiled javascript files together and injects them into the body of the index.html page so the scripts can be loaded and executed by the browser.

Monkey patching

Monkey patching is a technique used to alter the behaviour of an existing function either to extend it or change the way it works. In JavaScript this is done by storing a reference to the original function in a variable and replacing the original function with a new custom function that (optionally) calls the original function before/after executing some custom code.

The fake backend is organised into a top level handleRoute() function that checks the request url and method to determine how the request should be handled. For fake routes one of the below // route functions is called, for all other routes the request is passed through to the real backend by calling the original fetch request function (realFetch(url, opts)).

Nav component

Path: /components/Nav.jsx

The nav component displays the main navigation in the example. The custom NavLink component automatically adds the active class to the active nav item so it is highlighted in the UI.

Navlink component

Path: /components/NavLink.jsx

Next.js api handler

Path: /helpers/api/api-handler.js

Next.js config

Path: /next.config.js

The Next.js config file defines global config variables that are available to components in the Next.js app. It supports setting different values for variables based on environment (e.g. development vs production).

serverRuntimeConfig variables are only available to the API on the server side, while publicRuntimeConfig variables are available to the API and the client React app.

Next.js global error handler

Path: /helpers/api/error-handler.js

The global error handler is used catch all errors and remove the need for duplicated error handling code throughout the Next.js JWT auth api. It’s added to the request pipeline in the API handler wrapper function.

Next.js jwt middleware

Path: /helpers/api/jwt-middleware.js

The JWT middleware uses the express-jwt library to validate JWT tokens sent to protected API routes, if a token is invalid an error is thrown which causes the global error handler to return a 401 Unauthorized response. The middleware is added to the Next.js request pipeline in the API handler wrapper function.

Next.js project structure

The project is organised into the following folders:

JavaScript files are organised with export statements at the top so it’s easy to see all exported modules when you open a file. Export statements are followed by functions and other implementation code for each JS module.

The index.js files in some folders (components, helpers, services) re-export all of the exports from the folder so they can be imported using only the folder path instead of the full path to each file, and to enable importing multiple modules in a single import (e.g. import { errorHandler, jwtMiddleware } from ‘helpers/api’).

Prerequisites

To follow along, you’ll need:

Now, let’s start authenticating!

Private route

Path: /src/_components/PrivateRoute.jsx

Protected routes with react router

Create a folder called routing in src and a file named ProtectedRoute.js. ProtectedRoute is intended to be used as a parent route element, whose child elements are protected by the logic residing in this component.

React redux’s usedispatch and useselector hooks

By using useSelector and useDispatch from the react-redux package you installed earlier, you can read state from a Redux store and dispatch any action from a component, respectively.

React router’s onenter callback

The onEnter callback function can be added to any <Route>. It will be automatically invoked when the route is about to be entered. It provides the next router state, available as an argument, nextState, and a function to redirect to another path.

Reducers

Redux makes it very clear that the application’s data itself should never be mutated directly. Instead, a function should be put in place that returns the next state by looking at the previous state, along with an action that describes how things should change.

These functions are called reducers and are at the heart of Redux. It’s important to note that reducers should be kept pure, meaning that their output should rely solely on the arguments passed to them with no side effects such as making an API call or mutating the arguments passed in.

So why should reducers be pure and not have side effects? In short, it’s to keep things simple and predictable. A function that relies only on the arguments passed to it to derive the next state will be easier to reason about and debug. If we wanted to we could return mutated objects and Redux wouldn’t throw errors, but as mentioned, it is strongly discouraged.

Redux store

Path: /src/_store/index.js

Redux store (src/helpers/store.js)

Creates a Redux store that holds the complete state tree of your app. There should only be a single store in your app.

import { createStore, applyMiddleware } from‘redux’;

importthunkMiddlewarefrom‘redux-thunk’;

import { createLogger } from‘redux-logger’;

importrootReducerfrom‘../reducers’;

 

constloggerMiddleware = createLogger();

 

exportconststore = createStore(

  rootReducer,

  applyMiddleware(

    thunkMiddleware,

    loggerMiddleware

  )

);

Register reducer (src/reducers/register.reducer.js)

This Reducer manages the registration section of the application state.

import { appConstants } from‘../helpers/app-constants’;

 

exportfunctionregister(state = {}, action) {

  switch (action.type) {

    caseappConstants.REGISTER_REQUEST:

    return { registering:true };

    caseappConstants.REGISTER_SUCCESS:

    return {};

    caseappConstants.REGISTER_FAILURE:

    return {};

    default:

    returnstate

  }

}

Register.js (src/components/auth/register.js)

importReact, { Component } from‘react’;

import {Link} from‘react-router-dom’;

import { connect } from‘react-redux’;

 

import { userActions } from‘../../actions’;

 

classRegisterextendsComponent {

constructor(props) {

    super(props);

 

    this.state = {

      email:,

      password:,

      firstName:,

      lastName:,

      userName:,

      submitted :false

    };

    this.handleInputChange = this.handleInputChange.bind(this);

    this.submitRegister = this.submitRegister.bind(this);

  }

 

  handleInputChange(e) {

    letname = e.target.name;

    letvalue = e.target.value;

    this.setState({

      [name]:value

    });

  }

 

  submitRegister(e) {

    e.preventDefault();

    this.setState({ submitted:true });

    const { firstName, lastName, userName, email, password } = this.state;

    if (email && password) {

      this.props.register({firstName, lastName, userName, email, password});

    }

  }

 

  render(){

    const { registering } = this.props;

const { firstName, lastName, userName, email, password, submitted } = this.state;

    return(

 

      <divclassName=“col-md-4 col-md-offset-4”>

      <h2className=“text-center”>User Registration</h2><form>

      <divclassName={‘form-group’ (submitted && !firstName ? ‘ has-error’ : )}>

          <label>First Name:</label>

          <inputtype=“text”id=“firstName”className=“form-control input-shadow”placeholder=“Enter First Name”value={this.state.firstName}onChange={this.handleInputChange}name=“firstName”/>

                       {submitted && !firstName && <divclassName=“help-block”>First Name is required</div>}

      </div>

      <divclassName={‘form-group’ (submitted && !lastName ? ‘ has-error’ : )}>

          <label>Last Name:</label>

          <inputtype=“text”id=“lastName”className=“form-control input-shadow”placeholder=“Enter Last Name”value={this.state.lastName}onChange={this.handleInputChange}name=“lastName”/>

                      {submitted && !lastName && <divclassName=“help-block”>Last Name is required</div>}     </div>

      <divclassName={‘form-group’ (submitted && !userName ? ‘ has-error’ : )}>

          <label>Username:</label>

          <inputtype=“text”id=“userName”className=“form-control input-shadow”placeholder=“Enter user name”value={this.state.userName}onChange={this.handleInputChange}name=“userName”/>

                      {submitted && !userName && <divclassName=“help-block”>username is required</div>}

      </div>

      <divclassName={‘form-group’ (submitted && !email ? ‘ has-error’ : )}>

          <label>Email:</label>

          <inputtype=“text”id=“email”className=“form-control input-shadow”placeholder=“Enter Email”value={this.state.email}onChange={this.handleInputChange}name=“email”/>

                      {submitted && !email && <divclassName=“help-block”>Email is required</div>}

      </div>

      <divclassName={‘form-group’ (submitted && !password ? ‘ has-error’ : )}>

          <labelfor=“password”>Password: </label>

          <inputtype=“password”id=“password”className=“form-control input-shadow”placeholder=“Enter Password”value={this.state.password}

          onChange={this.handleInputChange}name=“password”/>

          {submitted && !firstName && <divclassName=“help-block”>Password is required</div>}

          </div>

          <buttontype=“button”onClick={this.submitRegister}className=“btn btn-primary btn-block”>Register</button>

      <Linkto=“/login”className=“btn btn-link”>Login</Link>

     </form></div>

    )

  }

 

}

 

functionmapState(state) {

const { registering } = state.register;

return { registering };

}

 

constactionCreators = {

register:userActions.register

}

 

constconnectedRegister = connect(mapState, actionCreators)(Register);

export { connectedRegisterasRegister };

Run the react redux app with a .net api

For full details about the example .NET JWT Auth API see the post .NET 6.0 – JWT Authentication Tutorial with Example API. But to get up and running quickly just follow the below steps.

Run the react redux app with a node.js api

For full details about the example Node.js JWT Auth API see the post NodeJS – JWT Authentication Tutorial with Example API. But to get up and running quickly just follow the below steps.

Sign out:

To add Sign out logic, we just have to create a SignOut component which would trigger specific action. Sample action creator for signout may looks like this:

export function signOutAction() {
  localStorage.clear();
  return {
    type: UNAUTHENTICATED
  };
}

The navbar and login components

We should see if everything is wiring up properly in the browser at this point. But before we do, we’ll need the Navbar and Login components in place.

The redux authentication actions

The actions that we need in our case are all going to be asynchronous because we are calling an API. To handle the async calls, we need a setup that has actions which cover the three possible states that exist:

  1. A request was sent
  2. A request successful
  3. A request failed

Let’s create our actions to cater to those.

The session reducer, log out edition

import * as types from '../actions/actionTypes';
import initialState from './initialState';
import {browserHistory} from 'react-router';

export default function sessionReducer(state = initialState.session, action) {
  switch(action.type) {
    case types.LOG_IN_SUCCESS:
      browserHistory.push('/')
      return !!sessionStorage.jwt
    case types.LOG_OUT:
      browserHistory.push('/')
      return !!sessionStorage.jwt
    default: 
      return state;
  }
}

See that our reducer responds to the LOG_OUT action by redirecting to the root path and creating a new copy of state with the session property set to false (assuming we’ve properly removed the JWT from sessionSotrage).

And that’s it! This new state will cause the Header component to re-render, properly displaying the “log in” link in the navbar.

Update app component (src/app.js)

importReact, { Component } from‘react’;

import { Router, Route } from‘react-router-dom’;

 

import {Login} from‘./components/auth/Login’;

import { Register } from‘./components/auth/Register’;

import {Home} from‘./components/Home/Home’;

 

import { history } from‘./helpers’;

import { PrivateRoute } from‘./components/PrivateRoute’

import { connect } from‘react-redux’;

import { alertActions } from‘./actions’;

import‘./App.css’;

 

classAppextendsComponent {

constructor(props) {

super(props);

 

history.listen((location, action) => {

// clear alert on location change

this.props.clearAlerts();

});

}

 

render() {

const { alert } = this.props;

return (

<div>

{alert.message &&

<divclassName={`alert ${alert.type}`}>{alert.message}</div>

}

<Routerhistory={history}>

<div>

<PrivateRouteexactpath=“/”component={Home}/>

<Routepath=“/login”component={Login}/>

<Routepath=“/register”component={Register}/>

</div>

</Router>

</div>

);

}

}

 

functionmapState(state) {

const { alert } = state;

return { alert };

}

 

constactionCreators = {

clearAlerts:alertActions.clear

};

 

constconnectedApp = connect(mapState, actionCreators)(App);

export { connectedAppasApp };

What is redux and what does it solve?

JavaScript applications are, in a lot of ways, large collections of data and state. Any good application will need a way for its state to be changed at some point, and this is where Redux comes in. Built by Dan Abramov, Redux is essentially a state container for JavaScript apps that describes the state of the application as a single object. Further, Redux provides an opinionated pattern and toolset for making changes to the state of the app.

Инициализация

Во-первых, нам нужно создать роутер, сага-мидлевар, стор и запустить сагу.

Компонент main

Main является компонентом контейнера без состояния, который будет перенаправлять на Login, если нет токена.

// Main.js
import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import { Route, Redirect } from 'react-router-dom';

const Main = ({ token }) => {
  if (!token) {
    return <Redirect to="/login" />;
  }
  return <div>Вы вошли в систему.</div>;
};

const mapStateToProps = (state) => ({
  token: state.auth.token
});

export default connect(mapStateToProps)(Main);

Редьюсер

Состояние приложения будет состоять из двух частей: одно для auth и одно для роутера. Редьюсер auth будет обрабатывать события аутентификации.

// reducer.js
import { combineReducers } from 'redux'
import { routerReducer } from 'react-router-redux';

export const AUTH_REQUEST = 'AUTH_REQUEST';
export const AUTH_SUCCESS = 'AUTH_SUCCESS';
export const AUTH_FAILURE = 'AUTH_FAILURE';

export const authorize = (login, password) => ({
  type: AUTH_REQUEST,
  payload: { login, password }
});

const initialState = {
  token: localStorage.getItem('token'),
  error: null
};

const authReducer = (state = initialState, { type, payload }) => {
  switch (type) {
    case AUTH_SUCCESS: {
      return { ...state, token: payload };
    }
    case AUTH_FAILURE: {
      return { ...state, error: payload }
    }
    default:
      return state;
  }
};

const reducer = combineReducers({
  auth: authReducer,
  router: routerReducer
});

export default reducer;

Роутер

Мы используем роутер с двумя маршрутами, один для компонента Login и один для компонента Main.

//App.js
import React from 'react';
import { Route, Switch } from 'react-router-dom';
import { ConnectedRouter } from 'react-router-redux';

import Main from './Main';
import Login from './Login';

const App = props => {
  const { history } = props;

  return (
    <ConnectedRouter history={history}>
      <Switch>
        <Route path="/login" component={Login} />
        <Route path="/" component={Main} />
      </Switch>
    </ConnectedRouter>
  );
};

export default App;

Fake backend api

The React Redux example app runs with a fake backend by default to enable it to run completely in the browser without a real backend API (backend-less), to switch to a real backend API you just have to remove or comment out the 2 lines below the comment // setup fake backend located in the main index file (/src/index.js). You can build your own API or hook it up with the .NET or Node.js API available (instructions below).

Updating initial state

Our initial state is set in src/reducers/initialState.js.

Let’s add a property, session. The value of this property will be determine by whether or not there is a jwt key/value pair in sessionStorage. session should point to true if such a pair is present, false if not.

// src/reducers/initialState.js

export default {
  cats: [],
  hobbies: [],
  session: !!sessionStorage.jwt
}

Next up, we’ll teach our session reducer how to update the state’s session property when receiving the LOG_IN_SUCCESS action.

Dependencies and build process

Here’s our dependencies in package.json:

Conclusion

In my opinion, Redux Toolkit delivers a better developer experience, especially compared to how difficult Redux used to be before RTK’s release.

I find Toolkit easy to plug into my applications when I need to implement state management and don’t want to create it from scratch using React.Context.

Storing tokens in WebStorage, i.e localStorage and sessionStorage, is also an important discussion. I personally find localStorage safe when the token has a short life span and doesn’t store private details such as passwords, card details, etc.

Feel free to share how you personally handle frontend authentication down in the comments!

LogRocket: Full visibility into your web and mobile apps

LogRocket Dashboard Free Trial Banner

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page web and mobile apps.

Try it for free

.

Fake backend

Path: /src/_helpers/fake-backend.js

Backend architecture

Our Express server, hosted on localhost:5000, currently has three routes:

Now we can move on to writing Redux actions, starting with the register action.

Похожее:  Личный кабинет Госуслуги Ухта – Официальный сайт, Вход, регистрация

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *