DEV Community

Cover image for Journey to fullstack developer - Authentication with Redux - promise based.
Osita Chibuike for Legobox

Posted on

Journey to fullstack developer - Authentication with Redux - promise based.

This article was originally posted on my blog at legobox

The weeks so far.

In the bid, to be frank, I'm going to write with respect to my own laziness, yes I have been really lazy about this project. I have a problem of not usually completing passion projects but I'm trying to change that which is why I have to actually write about my laziness too. In the bid to get my full experience across. Sometimes I actually think I don't like coding so much if there's an easier way to solve a problem I'd probably take it.

So far spectre has been undergoing slow progress, thanks to me (being sarcastic here), But I'm sure to improve that. This week completed all authentication related proceedings and I had the opportunity to really have a good look at authentication in react based projects and come up with some overview of the process, which I think I'd love to share.

Building out Authentication - Redux based

There have really been a lot of articles written about Authentication as with that came many idiosyncrasies with relation to the process, a lot of the time there were not enough explanations as to handle some scenarios and in other instances, these ideas were not production ready.

On one hand, we had the articles which do well to describe how it would be implemented but fail to account for how best to set up the system so that it's able to track the authentication status with relation to the token state provided by the server. i.e what if the state of the application says this user is logged in, but the token is expired, and also what if the case is reversed. and heck there were other things to think about.

  • Do redirections take place in the components or in the actions?
  • How do we handle asynchronous operations from actions?
  • What the best practices when working with redux.

At the end of the process, I came to the following conclusions,

  1. Promises are the way to go in handle redux based asynchronous actions, they are possibly best for all redux based actions.
  2. Actions are meant to focus on just performing actions of communicating with the servers and changing state, nothing else.
  3. Route changes have to happen in the components only, and nowhere else.
  4. All APIs actions have to be managed by an API class powered by axios. since its promise based, it fits directly into the promise based architecture of the system.
  5. Since we are taking all things asynchronously, use the async-await pattern where possible for more intuitive syntax.

Relation between tokens and states.

In the process of monitoring state, there 2 major properties that decided if a user was logged in or not.

  • The token received from the server meant to serve as the access token.
  • The state of the isAuthenticated state property true or false. This property is dependent on the token property.

With these two there were a few combinations to monitor when doing an auth check.

  • isAuthenticated = true, the token is found (User is actually logged in)
  • isAuthenticated = false, the token is found (User is logged in, but for some reason, the state is messed up)
  • isAuthenticated = true, the token is expired (User is not logged in on the server anymore, so we have to correspond)
  • isAuthenticated = false, the token is expired (User is really logged out ).

I wrote an article in the past about a structure of this sort, it explains the details but in this process I changed a few things from that article, I think I would have to update it, but here are a few things to note, if you haven't read that article, there's a link to it. It would help you understand what's going on here.

From that article there were 3 major components handling routing.

  • AppCheck container - Taking charge of updates on the routes to note if the user is logging in or logging out, in order to do the appropriate redirect.
  • EnsureLoggedInContainer - making sure the user visiting these routes is authenticated
  • EnsureVisitorContainer - making sure the user visiting these routes is a visitor(not logged in).

Well without further ado here are my proposed changes to these components.

On AppCheck component, I added a componentDidMount lifecycle hook to check for the presence of the token in relation to the state and make the appropriate changes to the state.

componentDidMount(){
    const { currentURL, pages, dispatch, isLoggedIn } = this.props;

    // note isLoggedIn is mapped directly to isAuthenticated by redux connect

    let token = this.props.cookies.get('uat');
    console.log(token);
    switch (isLoggedIn){
      case true: 
        if(!token){
          dispatch(actions.logout()); // this action automatically changes the state of isAuthenticated to false
        }
        break
      case false:
        if(token){
          dispatch(actions.autoLogin()); // this action automatically changes the state of isAuthenticated to true
        }
        break;
      default:
        break;
    }
  }
Enter fullscreen mode Exit fullscreen mode

Apparently, this is the only change I needed to make to my checks on AppCheck the other checks (EnsureLoggedInContainer and EnsureVisitorContainer) pretty much remain the same.

Redux setup with promises.

In the project, all actions returned promises, well all actions that were going to be called by the component via the mapDispatchToProps function. Using the thunk middleware from the redux-thunk setup promise functionalities were provided and I was able to write action functions like this.

import actionType from '../action-type';
import {api} from './'

export const autoLogin = () => (dispatch, getState) => new Promise((resolve,reject)=>{
    dispatch({
        type: actionType.LOGGED_IN,
        payload: true
    })
    resolve('success');
}).catch(err=>{
    console.log("error here", err)
    throw err
});

export const signup = (params) => (dispatch, getState) => new Promise(async (resolve, reject)=> {
    // do cool stuff then resolve/reject
    try{
        let res = await api.post('/auth/register', params)
        dispatch(autoLogin()).then(()=>{
            resolve({status:'success',message:'You successfully signed up!!!', ...res});
        }).catch(err=>{
            reject({status:'error',message:'Something went wrong!!!', ...res});
        })
    }catch(err){
        console.log(err)
        reject(err);
    }
}).catch(err=>{
    console.log(err)
    throw err
});
Enter fullscreen mode Exit fullscreen mode

As you can see signup function and autoLogin both use promises to handle their operations. Thus allowing us to defer operations based on these actions until they were completed. In order to interact with API endpoints, I wrote up a class called API for interacting with the APIs via axios.

import axios from 'axios'

let headers = {}, running = 0;

class API{

    SIGNUP = ''
    LOGIN =  ''
    CREATE_PROJECT = ''

    constructor(options = {}){
        this.axios = axios.create(options);
        this.headers = {}
    }

    send(type, args) {
        running++;
        const respond = (func, resp) => {
            running--;
            if (!resp) resp = {};
            func(resp.data, resp.headers, resp.status);
        };
        return new Promise(
            function(resolve, reject) {
                this[type]
                    .apply(this, args)
                    .then(resp => {
                        respond(resolve, resp);
                    })
                    .catch(error => {
                        let resp = error.response;
                        respond(reject, resp);
                    });
            }.bind(this.axios)
        );
    }

    get(){
        return this.send('get', arguments)
    }

    post(){
        return this.send('post', arguments)
    }

    put(){
        return this.send('put', arguments)
    }

    patch(){
        return this.send('patch', arguments)
    }

    delete(){
        return this.send('delete', arguments)
    }

    getHeaders(){
        return headers
    }

    getHeader(name) {
        return headers[name];
    }

    headerIs(name, value) {
        return headers[name] == value;
    }
    setHeaders(new_headers) {
        headers = new_headers;
        return this.renew();
    }

    setHeader (key, value) {
        this.axios.defaults.headers.common[key] = value
        return this
    }

    removeHeader(name, norenew) {
        delete headers[name];
        if (!norenew) this.renew();
        return this;
    }
    renew() {
        this.axios = axios.create({
            // baseURL: process.env.API_BASE_URL,
            headers: this.headers
        });
        return this;
    }
}

export default API;
Enter fullscreen mode Exit fullscreen mode

At its base its a simple class actually for manipulating headers and making sure to provide the proper bearer token on all requests, since axios changes are effected across the app, changes on this class run through the entire project via one instance defined in the index file

const api = new API({
    baseURL : env.API_BASE_URL,
})
Enter fullscreen mode Exit fullscreen mode

This is the instance imported in all actions.

Toasts and Notifiers.

It's good UI to let the users know something has happened or something is happening, or if an error occurred. With the promised based flow its usually really easy to catch these errors and since all actions "then" and catch phrases are handled in the components, we can essentially use a component-based toast handler or notifier.

In the project, I opted for the react-toastify plugin component its available on npm.

Conclusions

This week would complete all respective frontend implementation hopefully, I really do hope to make this count (putting on my clown nose). After this phase the APIs would be explored and I'd get into the nitty-gritty of how spectre works with relation to version control systems and servers.

Top comments (0)