Implementing a smart Login Modal with Redux, reselect and ReactJS

Requirement

  1. Implement a mechanism that popup a login modal when the user’s session is expired.
  2. After the user logged in again, all his original requests to the server should be executed.

How?

I will use Redux, reselect, ImmutableJS and ReactJS for the implementation (ECMAScript 6).

All users’ requests should be stored in a retries queue to be execute after a successful login.


For your convenience, I created a working repository of the implementation:
https://github.com/dorsha/login-modal-react-redux

Let’s go over Redux basics and implement the requested mechanism.

Actions

Actions are payloads of information that send data from your application to your store. They are the only source of information for the store. You send them to the store using store.dispatch().

Each action must have a type property that indicates the type of action being performed (string constants).

Let’s create our first Action Type - Login required:

export default {
loginRequired: 'LOGIN_REQUIRED'
};

And our first Action - Login required (for the above type):

export function loginRequired(request) {
return {
type: ActionTypes.loginRequired,
payload: {
request: request
}
};
}

You can also have an action for errors:

{   
type: 'LOGIN_REQUIRED',
payload: new Error(),
error: true
}

Action may also have a ‘meta’ property (any extra information that is not part of the payload).

It’s a good idea to pass as little data in each action as possible.

Action creators are functions with zero side-effects, that all it does is to return an action.

Now we can dispatch the our action like this:

dispatch(loginRequired(request))

You can also create a bound actions creator to automatically dispatches:

const boundLoginRequired = (request) => dispatch(loginRequired(request))

and then:

boundLoginRequired(request)

Reducers

The job of the reducer is to change the application’s state according to the action.

In Redux, all application state is stored as a single object. You should always think about the minimal representation of the state as an object.

You should keep every entity in an object stored with an ID as a key, and then use this ID to reference it from other entities.

After you decided how your state object looks like, you need to write a reducer for it. The reducer is a function that takes an action and state and returns the next state according to the action (that’s it).

Things that are not allowed to do inside the reducer function:

  • Mutate its arguments
  • Perform side effects like API calls
  • Calling non-pure functions (like Date.now())

Each reducer should have the initial state of the object.

In our reducer we have an initial state and actions handlers which are a map from action to function (I use a reducer creator to do all the checks for us. For example, it will initial the state with the initial state when the state is undefined).

For our implementation, we should have an initial state and a reducer function:

Initial state (login required set to false and empty retries queue):

export const initialState = Immutable.fromJS({
loginRequired: false,
retriesQueue: []
});

You can see that I use ImmutableJS for creating the initial state, this will help us later when we want to return a new merged state.

Reducer function:

[ActionTypes.authentication.loginRequired]: (state, action) => {
var newState = state;
... magic here ...
return newState;
}

For the LOGIN_REQUIRED action’s type we created a reducer that receive the previous state and an action with a ‘request’ inside its payload.

Now let’s implement the reducer logic.
We want to mark the next state as login required and add the retry request to it.

[ActionTypes.loginRequired]: (state, action) => {
var request = action.payload.request;
var newState = state.update('loginRequired', () => true);
newState = newState.update('retriesQueue',
() => Immutable.List.of(request));
return newState;
}

As you already know, you are not allowed to mutate the state, so you need to merge the previous state with the new state.

You can use ImmutableJS for this kind of job, for example:

var newState = state.updateIn(['key', 'path'], data)
var newState = state.merge(newState)

Which always returns a new object and does not mutate the given object.

Until now we have a loginRequired action that represent the “what happened” and a loginRequired reducer that update the state according to the actions.

Stores

Store is an object that brings actions and reducers together.

The store responsibilities:

  • Holds application state
  • Allows access to state via getState()
  • Allows state to be updated via dispatch(action)
  • Registers listeners via subscribe(listener) - or selectors (later…)

You should have only single store in a Redux application. When you want to split your data handling logic you will use reducer composition instead of many stores.

Data lifecycle

  1. Calling store.dispatch(action) - You can call it from anywhere in the app (including components, XHR callbacks or even at scheduled intervals
  2. Redux store calls the corresponding reducer function for getting the next state according to an action and a previous state
  3. The root reducer combines the output of multiple reducers into a single state tree
  4. The redux store saves the complete state tree returned by the root reducer

The new tree is now the next state of the app. Each listener registered with store.subscribe(listener) will now be invoked; Listeners may call store.getState() to get the current state.

Let’s go back to our implementation.

In order to “call” the reducer we have to:
Call the store.dispatch(…) with the LOGIN_REQUIRED action when the user is not authenticated anymore.

  • The original user’s request need to be dispatched together with the action (to be executed later)
  • A promise for this request is returned to the caller and will be resolve/reject later by the request.
if (response.status === HttpCodes.UNAUTHORIZED) {
var request = this.request;
var promise = new Promise((resolve, reject) => {
request.resolve = resolve;
request.reject = reject;
});
store.dispatch(loginRequired(request)); // dispatch our action
return promise;
}

The redux store will save the complete new state.

Now, we have two ways to receive the new data:

Subscribers

We can add a store.subscribe(listener) to our LoginModal component, that will be invoked in each store change, and will popup a login modal.

this.unsubscribe = store.subscribe(() => {
// open login modal if login required
var loginRequired =
store.getState().application.getIn(['loginRequired']);
if (loginRequired) {
var retriesQueue =
store.getState().application.getIn(['retriesQueue']);
this.setState({retries: retriesQueue}); // To be invoked later
openModal();
}
}
);

This way is not efficient, since we should have a lot of subscribers in our code and check the state, which is not elegant and hard to test.

Selectors (reselect)

Selectors can be used to compute derived data from the state, this allow Redux to store the minimal possible state.

Selector is connected to the Redux store using the connect() function.

Selectors are efficient, will be recomputed just when one of its arguments change.

Therefore, each component that requires specific data from the state should have its own selector to get the relevant manipulated data.

In our implementation we will create a login required selector that returns the ‘loginRequired’ and ‘retriesQueue’ values from the state (as is).

import { createSelector } from 'reselect';

const applicationSelector = (state) => state.application;
export const loginRequiredSelector = createSelector(
applicationSelector,
(application) => ({
loginRequired: application.get('loginRequired'),
retriesQueue: application.get('retriesQueue')
})
);

Now we need to connect our ‘loginRequiredSelector’ to the LoginModal component. All keys from selector results are available on the props object for the bounded component.

In our example, the result keys of the selector are ‘loginRequired and ‘retriesQueue’ which will be injected into the LoginModal component by the connect() call.

render() {
const { loginRequired } = this.props;

if (loginRequired) {
openModal();
}

return (
<div className="login ui basic small modal">
<Login modalCallback={this.onLoginModalClose.bind(this)} />
</div>
);
}
// These props injected by the connect() from the selector
LoginModal.propTypes = {
loginRequired: React.PropTypes.bool,
retriesQueue: React.PropTypes.object
};
// Connect the loginRequiredSelector to the LoginModal component
export default connect(loginRequiredSelector)(LoginModal);

After a successful login:

  • The login modal will be closed
  • All requests in the queue will be invoked
  • Promises will be resolved/rejected
onLoginModalClose() {
// iterate over all retries, execute them and resolve the promise
const { retriesQueue } = this.props;
retriesQueue.forEach((retry) => {
retry()
.then((data) => {
retry.resolve(data);
})
.catch((ex) => {
retry.reject(ex);
});
});

// close modal
closeModal();
}

That’s it, we now have a mechanism that opens a login modal when the user is no longer authenticated to the server, store the original user’s requests and retry all of them after a successful login.

Let’s “see” it working

I have an archive button that what it does is to call the ‘/archive’ server API.

Archive button that calls the /archive API (on click)

Now, let’s say I am no longer authenticated to the server and try to press the archive button.

I got Unauthorized response after I try to call the server (archive API), and our LOGIN_REQUIRED action is dispatched
A login modal is popped up (login required is now true)
After a successful login, the original request (archive) is called from the retries queue

Written by Doron Sharon (http://www.dorsha.com)