Decoupling Auth Events with RxJS & Redux-Observable

Nolan Phillips
4 min readMar 29, 2017

--

At Forestry, we grab several datasets and hookup other systems when a user logs into the CMS. A few of the tasks that must be run include:

  1. Authorizing the user and store their auth_token for future use.
  2. Loading the user data.
  3. Loading their Jekyll/Hugo site data.
  4. Loading the index of site pages.
  5. Opening a websocket to Forestry.

Only one of these things is directly related to logging in, so how can we setup the app to do all of these things in a reasonable way?

I’m going to look at how this can be accomplished in a way that satisfies two of the principles of SOLID design: the Single Responsibility Principle (SRP), and the Open-Closed principle (OCP).

First, we’ll discuss using Container Components & redux-thunk to carry out these actions in the proper order. This is the method we first used at Forestry.io for the admin. As you’ll see, this method gets the job done and decouples the UI from the process, but fails to comply with our two principles.

The second method uses rxjs and redux-observable to decouple authorization from the other parts of the setup process .

Container Components & redux-thunks

A good pattern for React/Redux development, is to split your components into Presentation and Container Components. Presentation components are pure-functions that render content based only on their inputs, and do no complex processing. Container components fetch the data needed by the Presentation components and provide the callbacks they expect for user interaction.

We’re going to provide a LoginForm presentation component that accepts only a login function, which is called when the submit button is clicked.

LoginForm.jsx

const valueOf = (id) => document.getElementById(id).value;export const LoginForm = ({ login }) => (
<div>
<h2>Login</h2>
<label>Username</label>
<input id="username" type="text" />
<label>Password</label>
<input id="password" type="password" />
<button
value="Submit"
onClick={() => {
const username = valueOf('username');
const password = valueOf('password');
login(username, password);
}}
/>
</div>
);

A LoginFormContainer component will define the login function as a chain of thunks–functions that can be dispatched as actions. These function actions are given the store’s state and dispatch function so they can dispatch more actions. This lets us write actions that can carry out async actions. Dispatching a thunk returns the promise, allowing us to chain them together. Here’s an example of using this pattern, we might create an auth thunk creator:

my-api.js

export function auth(username, password) {
return function thunk(dispatch, getState) {
dispatch({ type: 'AUTH_REQUEST' })
return $.post('/login', { username, password })
.then(() => dispatch({ type: 'AUTH_SUCCESS' }))
.catch(() => dispatch({ type: 'AUTH_FAILURE' }))
}
}

When called, auth will return the thunk to be passed to the Redux store. That function will be called with access to the store, so it can dispatch actions when the request begins and either succeeds or fails. With this thunk-creator defined, we can use the Container to connect it to the LoginForm.

LoginFormContainer.jsx

import { auth, loadUser, loadSite, loadPages } from 'my-api';
import { openWebsockets } from 'my-websockets';

const mdtp = {
auth, loadUser, loadSite, loadPages, openWebsockets
};
export const LoginFormContainer = connect(null, mdtp)((props) => { let {auth, loadUser, loadSite, loadPages, openWebsockets} = props return(
<LoginForm
login={(username, password) => {
auth(username, password)
.then((token) => {
loadUser(token);
openWebsockets(token);
loadSite(token)
.then((site) => {
loadPages(site, token)
});
});
}}
/>
)
});

This isn’t a bad start. The presentation of our form is separated from the details of what happens when the form is submitted. Still, the Container is in pretty strong violation of both SRP and OCP. This is evident because the login function does more than just login, so any changes to this process are going to require the LoginFormContainer to be opened up again.

Decoupling with Redux-Observable

Another wonderful middleware we discovered is redux-observable. This library allows you to register functions called epics, which observe the stream of actions passing through redux store.

The premise behind epics is simple–actions in, actions out–and because it’s based on RxJS, redux-observable is a quite powerful library. But we're going to use it very plainly.

Our LoginForm component is going to remain the same, but we're going to stop chaining other actions onto the auth thunk. This lets us simplify the LoginFormContainer quite a bit.

LoginFormContainer.js

import { auth } from 'my-api';
import { LoginForm } from 'LoginForm';
const mdtp = { auth };export const LoginFormContainer = connect(null, mdtp)((props) => { let { auth } = props return <LoginForm login={auth} />
});

The important thing to note here is that our LoginFormContainer does only what it says it should: it authorizes the user.

Now we can create several epics to trigger the actions that occur after a user is logged into the CMS.

login-epics.js

import { auth, loadUser, loadSite, loadPages } from 'my-api';
import { openWebsockets } from 'my-websockets';
export const loadUserOnAuth = (action$) =>
action$
.ofType('AUTH_SUCCESS')
.mapTo((action) => loadUser(action.token));
export const openWebsocketsOnAuth = (action$) =>
action$
.ofType('AUTH_SUCCESS')
.mapTo((action) => openWebsockets(action.token));
export const loadSiteAndPagesOnAuth = (action$) =>
action$
.ofType('AUTH_SUCCESS')
.mergeMap((action) => (
loadSite(action.token)
.then((site) => loadPages(site, token);
))

This method provides a solution to both of our guiding principles:

  • The Single Responsibility Principle is satisfied because the LoginFormContainer is completely ignorant of what happens after the the user has been logged in, and not just because we abstracted it away with a thunk. The newly created Epics also follow SRP, because they each do just one thing.
  • The Open Closed Principle is also satisfied. Extending the setup process with more epics does not require opening back up the LoginFormContainer or modifying any existing Epics. One simply needs to register a new Epic with the Redux Store to add this functionality.

Although it can take some time to get used to rxjs and redux-observable, they are certainly very cool and useful libraries. If the strange operators like ofType, mergeMap, and mapTo make you nervous, the RxSJ Docs have a great tool to help you find the operator you're looking for.

--

--

Nolan Phillips

Software developer at CareGuide and co-organizer of PEI Devs