Building an Angular 2 "reactive" auto logout timer with the redux pattern

I am continuing to learn how to apply a "reactive" way of thinking to problems I'm working to solve every day. One item that comes up is the need to automatically log a user out of an application after some period of inactivity. I have seen a few attempts at this, but it usually becomes pretty onerous once any level of complexity is reached. So I wanted to take a stab at this in the context of an Angular 2 application using RxJS.

To set the stage I'm going to build a simple app that uses @ngrx/store to provide a redux state model. This is a nice pattern for several reasons, but in this case it provides a single stream of data to observe to detect if the user is doing anything of consequence in the application. Here's what the demo looks like:

What's happening here is the following:

  • Once a user logs in the inactivity timer is started
  • The timer is reset any time a user clicks one of the buttons to change the counter
  • Once the user doesn't click a button for 5 seconds an event is dispatched to indicate the timeout occurred

Now in this example I'm making an assumption that changes in the global state are enough to know if a user is doing anything. This may not be the case for all applications, but we're just exploring the idea here.

The code

Let's walk through the meaningful code, but all of the code for this example is in my github repository.

In this code we're using @ngrx/store, so we need some items to implement the redux pattern. The first is the global state object itself, and in our case it's pretty simple. It just contains the counter value and a boolean to indicate whether a user is logged in or not:

export const initialState: IState = {  
    counter: 0,
    loggedIn: false
};


export interface IState {  
    counter: number;
    loggedIn: boolean;
}

Of course in a real application there'd be more complexity around whether a user is logged in or not, but the basic principal should still apply.

With the state we can then define our reducer, which contains both the actions available and what changes each has on the state:

import { ActionReducer, Action } from '@ngrx/store';  
import {IState, initialState} from "./state.model";

export const INCREMENT = 'INCREMENT';  
export const DECREMENT = 'DECREMENT';  
export const RESET = 'RESET';  
export const USER_LOGGED_IN = "USER_LOGGED_IN";  
export const USER_LOGGED_OUT = "USER_LOGGED_OUT";

// This is the action fired when the activity timeout occured.
//  We keep it generic so the reducer can take any number 
//  of actions based on it (including perhaps none)
//
export const ACTIVITY_TIMEOUT_OCCURRED = "ACTIVITY_TIMEOUT_OCCURRED";

export const appReducer: ActionReducer<IState> = (state: IState, action: Action) => {  
    switch (action.type) {
        case INCREMENT:
            return {
                counter: state.counter + 1,
                loggedIn: state.loggedIn
            } as IState;

        case DECREMENT:
            return {
                counter: state.counter - 1,
                loggedIn: state.loggedIn
            } as IState;

        case RESET:
            return {
                counter: 0,
                loggedIn: state.loggedIn
            } as IState;

        case USER_LOGGED_IN:
            return {
                counter: state.counter,
                loggedIn: true
            } as IState;

        case USER_LOGGED_OUT:
        case ACTIVITY_TIMEOUT_OCCURRED:
            return {
                counter: state.counter,
                loggedIn: false
            } as IState;

        default:
            return state;
    }
}

There's not much to our reducer, but note that we're treating the ACTIVITY_TIMEOUT_OCCURRED action the same as when a user logs out. Depending on your scenario you might treat it differently. For example, depending on the state of the app you might not always just log them out.

With the reducer in place we can implement the UI, and in our case we have a single app component:

import { Component } from '@angular/core';  
import {Observable} from "rxjs/Observable";

import {Store} from "@ngrx/store";

import {AutoLogoutService} from "./auto-logout.service";  
import {INCREMENT, DECREMENT, RESET, USER_LOGGED_IN, USER_LOGGED_OUT} from "./app.reducer";  
import {IState} from "./state.model";

@Component({
    selector: 'my-app',
    providers: [AutoLogoutService],
    styles: [`
        .flex-col {
            display: flex;
            flex-direction: column;
            margin-bottom: 20px;
        }

            .flex-row {
            display: flex;
            flex-direction: row;
        }
    `],
    template: `
        <div *ngIf="!(loggedIn$ | async)" class="flex-col">
            <span>There is no user logged in</span>
            <div class="flex-row">
                <button (click)="login()">Login</button>
            </div>
        </div>

        <div *ngIf="(loggedIn$ | async)" class="flex-col">
            <span>You are logged in</span>
            <div class="flex-row">
                <button (click)="logout()">Log out</button>
            </div>
        </div>

        <div class="flex-col">
            <span>Counter: {{counter$ | async}}</span>

            <div class="flex-row">
                <button (click)="decrement()" [disabled]="!(loggedIn$ | async)">Decrement</button>
                <button (click)="reset()" [disabled]="!(loggedIn$ | async)">Reset</button>
                <button (click)="increment()" [disabled]="!(loggedIn$ | async)">Increment</button>
            </div>
        </div>
    `
})
export class AppComponent {  
    counter$: Observable<number>;
    loggedIn$: Observable<boolean>;

    constructor(
        private store: Store<IState>,
        private autoLogoutService: AutoLogoutService
    ) {

        this.counter$ = store.select("counter") as Observable<number>;
        this.loggedIn$ = store.select("loggedIn") as Observable<boolean>;

    }

    decrement() {
        this.store.dispatch({ type: DECREMENT });
    }
    reset() {
        this.store.dispatch({ type: RESET });
    }
    increment() {
        this.store.dispatch({ type: INCREMENT });
    }

    login() {
        this.store.dispatch({ type: USER_LOGGED_IN });
    }

    logout() {
        this.store.dispatch({ type: USER_LOGGED_OUT });
    }
}

This isn't too complicated, and just provides the UI and has some methods to respond to the button clicks. We are injecting the AutoLogoutService here to make sure it's created, but you could use other methods if you needed to.

So the last bit is handling the actual inactivity timer, and here's where the fun rxjs stuff comes in. Our service is actually ridiculously simple, but I've provided a bunch of comments so I don't forget what's going on here:

import { Injectable } from '@angular/core';  
import {Observable} from "rxjs";  
import {Store} from "@ngrx/store";

import {ACTIVITY_TIMEOUT_OCCURRED} from "./app.reducer";

import {IState} from "./state.model";

@Injectable()
export class AutoLogoutService {

    constructor(
        store: Store<IState>
    ) { 
        // In our example we're treating any change in global state 
        //  as an example of user activity. So to start we need to 
        //  just get an observable for the stream of state changes
        //
        let state$ = store.asObservable() as Observable<IState>;

        // Now here's where the power of RxJS comes into play. We're going to user 
        //  a technique where we react to state changes by mapping them to a new 
        //  observable that emits a value after the timeour period expires.
        //
        state$
            // We only want to start an activity timer when a user is actually logged 
            //  in, so filter out any state changes where that flag is false.
            //
            .filter((x: IState) => x.loggedIn)
            //
            // Now we don't really care what the state change was in this context, and 
            //  what we really want is an observable for our specific timeout value. So 
            //  we map the state change to an observable that emits a single value after
            //  the timeout.
            //
            // Note, this also means that any state change we care about creates a *new* 
            //  observable...and this is how we "reset" the timer when that happens.
            //
            .map((x: IState) => Observable.timer(5000))
            //
            // This is just for logging purposes in the demo so we can see when a new 
            //  timer is started.
            //
            .do((x: any) => console.log("Activity detected! Timer has reset to 5 seconds"))
            //
            // Now here's the slick part (IMO). Each time a new "timeout" observable is 
            //  created we want to make sure it replaces any timer we were subscribed to 
            //  before. In other words, as soon as a new timer is started we want to use that 
            //  one and just ignore anything we had before.
            //
            // In RxJS that's a simple operator called switch().
            //
            .switch()
            //
            // Finally, we need to subscribe to the timer observable to decide what to do 
            //  should it actually fire (i.e., the timeout actually expired). Here we're 
            //  just going to dispatch a new action that indicates the timeout expired. Then
            //  the reducer can decide what it actually means based on the state of the 
            //  application at the time.
            //
            .subscribe((x) => {
                console.log("Inactivity interval expired! Dispatching timeout event")
                store.dispatch({type: ACTIVITY_TIMEOUT_OCCURRED});
            });

    }

}

I won't repeat the comments here, but with a couple lines of rxjs code we can implement the entire thing. I'm sure this is too simple for some use cases, but I think it's pretty incredible how easy this is once you understand what operators are available...which I think is the hardest thing about ReactiveX in general.

Note, there's a great page that details how to approach a number of use cases with ReactiveX.

Wrapping up

In this post I walked through an example of using redux and rxjs to create a timer that will log a user out automatically after some interval. This is a simple example, but I think with some more work this pattern could be easily used in more complicated scenarios.

Again, all of the code for this example is in my github repository.

If you have any thoughts, feedback or see a bug in what I have please let me know in the comments. Thanks for reading!

comments powered by Disqus