Search code examples
reactjsreduxredux-frameworkredux-thunk

Redux logging in


I have been looking for an elegant (and correct) way to handle logging in for a Redux application. Currently the the user is directed to a login component/container where he see's a form with username and password. Then when he clicks on submit I call an async login function and when the promise is fulfilled then I dispatch a loginSuccess and also a replacePath call. The following is some pseudo code:

submit(e) {
   login(username, password)
      .then(function (user) {
         dispatch(loginSuccess(user));
         dispatch(replacePath('/');
      });
}

This works but I'm not sure it's best practice. Anyone have any better implementations?


Solution

  • Its generally considered bad practice to call dispatch within a component unless its a top-level container connected to the store.

    I'd recommend following the examples that Dan Abramov gives in the docs, most notably the async Reddit post fetching example. Take a look at how he handles the interim of the request with posts.isFetching.

    Since I know StackOverflow doesn't like links, here's a simplified example (in ES6):


    These are the actions:

    // Actions
    
    import fetch from 'isomorphic-fetch';
    import * as types from '../constants/actionTypes.js';
    var requestAuth = function() {
        return {
            type: type.REQUEST_AUTH
        }
    };
    
    var authSuccess = function(response) {
        return {
            type: type.AUTH_SUCCESS,
            response: response
        }
    };
    
    var authFail = function(response) {
        return {
            type: type.AUTH_FAIL,
            response: response
        }
    };
    
    var authenticate = function(username, password) {
        var fetchOptions = {
            method: 'post',
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({username: username, password: password})
        };
        var uri = '/api/path/to/your/login/backend';
        return dispatch => {
            dispatch(requestAuth);
            return fetch(uri, fetchOptions)
                .then(response => {
                    if (resopnse.status === 200) {
                        dispatch(authSuccess(response));
                        // Do any other login success work here
                        // like redirecting the user
                    } else {
                        dispatch(authFail(response));
                    }
                }
        }
    };
    

    Next the reducer:

    // Reducer
    import { combineReducers } from 'redux';
    import { REQUEST_AUTH, AUTH_SUCCESS, AUTH_FAIL } from '../actions/login';
    
    function login(state = {
        isAuthenticating: false,
        isLoggedIn: false,
        authenticationToken: '',
        authError: null
        .....,   // whatever other state vars you need
        .....
        }, action) {
            switch(action.type) {
                case REQUEST_AUTH:
                    return Object.assign({}, state, {
                        isAuthenticating: true
                    });
                    break;
                case AUTH_SUCCESS:
                    return Object.assign({}, state, {
                        isAuthenticating: false,
                        isLoggedIn: true,
                        authenticationToken: action.response.token
                    });
                    break;
                case AUTH_FAIL:
                    return Object.assign({}, state, {
                        isAuthenticating: false,
                        authError: action.response.error
                    });
                break;
               default:
                    return state;
    
        }
    }
    

    And finally the component method

    // Component Method
    // authenticate() should be wrapped in bindActionCreators()
    // and passed down as a prop
    function handleSubmit(username, password) {
        if (isValid(username) && isValid(password) {
            authenticate(username, password);
        }
    }
    

    tl;dr Your user types in their credentials which should be part of state (not pictured here). An onClick in the component calls handleSubmit(), which dispatches authenticate(). Authenticate dispatches requestAuth() which updates state to show your user that the request is being processed (a loading spinner displays or something). Once your AJAX call to the backend returns with the authentication results, you dispatch either authSuccess() or authFail() to update state and inform the user whether their request succeeded or not.