Jul 14, 2018

I have really enjoyed working with React these days and most of the time found that as long as my views/components correlated 1:1 with my data model that I could get away with using the component state with no need for a state management library. More recently however, as my web apps have become more complex, I have found a true need for an external state management library that enables sharing state across multiple deeply nested components as well as better organizes logic for state. In addition to sharing state across multiple components, it is really beneficial to be able to keep components as pure as possible, essentially only rendering data that is passed into them and providing actions that can be called based on events that occur within the component (think of a button being clicked).

An Example App

In order to explore both Redux (with Redux-Observable for side effects) and MobX I decided to make a really simple example app that I would describe as a “tag selector”. Essentially there are three components: one component displays a list of persisted tags that come from an API of some sort, another component displays a list of selected tags which can be selected by clicking on the persisted tags explicitly, and another component has a text input in which a comma separated list of tags can be entered that will be parsed and show up in the selected tags area. There is also a button that enables saving the selected tags to the database so that they replace the persisted tags. This is a pretty simple application that could be easily pulled into another app in which tag selection is needed.

The entire state could technically be managed within the top level container housing all of these components and passed down as props to the other components, but there could be situations where you would want this functionality and the components would be much more isolated, perhaps 2-3 levels deep as child components. In that case, one component would be affecting the state of another component, and you likely wouldn’t want to explicitly pass props down all the way from the parent component. In that case, the store can be provided via a Provider and accessed via any child component by injecting it into the component.

Type Safety

I really enjoy using TypeScript because it provides a lot of abstractions as well as handy guarantees that you aren’t passing the wrong type into a method or generally making silly mistakes. You get instant feedback in your IDE by using TypeScript and can simply avoid a lot of simple bugs that could occur at runtime. No doubt, other bugs may exist, which is what tests are ultimately for. Either way, keep in mind that this post comes from the perspective of wanting to do UI state management in a type safe way.

Redux and MobX

I spent a lot of time playing with Redux which is probably the biggest kid on the block. Many people love its simplicity and the functional programming approach that is coupled with immutable data structures. Essentially anything that occur in the app is described by an action that can have an optional payload, when that action is called a reducer accepts the current state which is copied to a new state with the changes required by the action via a switch statement typically. You will see a lot of spread operators or use of external libraries to maintain immutable data structures. Because reducers are pure functions, side effects are then handled through a separate library like redux-thunk, redux-saga, or redux-observable (epics). Redux is great and has a large community behind it as well as some really cool dev tools. However, as you will see in this post there is a lot of boilerplate required to get it going with TypeScript.

MobX in contrast is a more imperative state management method, in which you create one or many Stores with fields that are observable by the observer components that are specifically decorated. There is a lot more “magic” going on, in that typically things just work without a lot of boilerplate. MobX is also a lot less opinionated, so you can definitely shoot yourself in the foot if you don’t maintain a particular paradigm. Just like in Redux, MobX has an action decorator which you can use to create methods that you call from within your components that modify the state of the store. In this case, typically the store state is mutable rather than immutable, which is why I would highly recommend that you only use actions defined in the store to mutate the state as it provides a lot more consistency and predicability.

The Redux Version

As mentioned above, I wrote a lot of code to help reduce the boilerplate significantly for type safe redux. There are a ton of libraries and ideas out there on the internet of how this should/can be done, and I heavily referenced this example with some small changes that you will see below.

First, the Redux type helpers as seen in ReduxTypes.ts:

export namespace ReduxTypes {
    interface Action<T> {
        type: T;
    }
    interface ActionWithPayload<T, P> extends Action<T> {
        payload: P;
    }

    type ActionFn<T extends string> = () => Action<T>;
    type ActionWithPayloadFn<T extends string, P> = (payload: P) => ActionWithPayload<T, P>;

    export function createAction<T extends string>(type: T): ActionFn<T>;
    export function createAction<T extends string, P>(type: T, payloadType: P): ActionWithPayloadFn<T, P>;
    export function createAction<T extends string, P>(type: T, payloadType?: P) {
        return payloadType === undefined ? () => ({ type }) : (payload: P) => ({ type, payload });
    }

    type FunctionType = (...args: any[]) => any;
    type IActions = {
        [actionCreator: string]: FunctionType;
    }

    export type ActionsUnion<A extends IActions> = ReturnType<A[keyof A]>;
    export type ActionsType<A extends IActions, T extends keyof A> = ReturnType<A[T]>;
}

This makes it really easy to define action creators as well as obtain the ActionsUnion type from our implementation of the action creators. Now let’s see what our store looks like, in TagStore.ts:

import { AppState } from './Store';
import { Observable } from 'rxjs';
import { ActionsObservable, ofType } from 'redux-observable';
import axios from 'axios';
import { ReduxTypes } from './ReduxTypes';

export namespace TagStore {

    export enum Types {
        SELECT_TAG = 'SELECT_TAG',
        SELECT_TAGS = 'SELECT_TAGS',
        DESELECT_TAG = 'DESELECT_TAG',
        PERSIST_TAGS = 'PERSIST_TAGS',
        PERSIST_TAGS_SUCCESS = 'PERSIST_TAGS_SUCCESS',
        LOAD_TAGS = 'LOAD_TAGS',
        LOAD_TAGS_SUCCESS = 'LOAD_TAGS_SUCCESS',
        ERROR = 'ERROR'
    }

    const Actions = {
        [Types.SELECT_TAG]: ReduxTypes.createAction(Types.SELECT_TAG, {} as string),
        [Types.SELECT_TAGS]: ReduxTypes.createAction(Types.SELECT_TAGS, {} as string[]),
        [Types.ERROR]: ReduxTypes.createAction(Types.ERROR, {} as string),
        [Types.DESELECT_TAG]: ReduxTypes.createAction(Types.DESELECT_TAG, {} as string),
        [Types.PERSIST_TAGS]: ReduxTypes.createAction(Types.PERSIST_TAGS, {} as string[]),
        [Types.PERSIST_TAGS_SUCCESS]: ReduxTypes.createAction(Types.PERSIST_TAGS_SUCCESS),
        [Types.LOAD_TAGS]: ReduxTypes.createAction(Types.LOAD_TAGS),
        [Types.LOAD_TAGS_SUCCESS]: ReduxTypes.createAction(Types.LOAD_TAGS_SUCCESS, {} as string[])
    }

    type Actions = ReduxTypes.ActionsUnion<typeof Actions>;
    type ActionsType<T extends keyof typeof Actions> = ReduxTypes.ActionsType<typeof Actions, T>;

    export interface State {
        persistedTags: string[];
        selectedTags: string[];
        error: string;
    }

    const initialState: State = {
        persistedTags: [],
        selectedTags: ["adam", "aj"],
        error: ""
    }
    export const store = (state: State = initialState, action: Actions): State => {
        switch (action.type) {
            case (Types.SELECT_TAG):
                //if tag already selected
                if (state.selectedTags.indexOf(action.payload) > -1) {
                    return state;
                }
                return { ...state, selectedTags: [...state.selectedTags, action.payload] };
            case (Types.SELECT_TAGS):
                return { ...state, selectedTags: action.payload };
            case (Types.DESELECT_TAG):
                const deselectedList = state.selectedTags
                    .filter(tag => tag != action.payload);
                return { ...state, selectedTags: deselectedList }
            case (Types.LOAD_TAGS_SUCCESS):
                return { ...state, selectedTags: [], persistedTags: action.payload }
            case (Types.ERROR):
                return { ...state, error: action.payload };
            default:
                return state;
        }
    }

    export interface PropsFromState {
        health: State;
    }
    //Could transform AppState to Health.State  here if necessary
    export const mapStateToProps = (state: AppState): State => {
        return {
            ...state.health
        }
    }

    export type Props = typeof mapDispatchToProps;

    export const mapDispatchToProps = {

        select: Actions[Types.SELECT_TAG],
        selectAll: Actions[Types.SELECT_TAGS],
        remove: Actions[Types.DESELECT_TAG],
        persist: Actions[Types.PERSIST_TAGS],
        load: Actions[Types.LOAD_TAGS]

    }

    export const persistTagsEpic = (action$: ActionsObservable<Actions>): Observable<Actions> =>
        action$
            .ofType(Types.PERSIST_TAGS)
            .mergeMap((action: ActionsType<Types.PERSIST_TAGS>) => {
                console.log(action.payload);
                const tagInJson = action.payload
                    .map((tag) => {
                        return { tag }
                    });
                console.log(tagInJson);
                return Observable.fromPromise(
                    axios.post("http://acostanza.com/tags-api/?key=secret_key&method=write", action.payload)
                        .then(res => {
                            console.log(res);
                            return true;
                        }).catch(e => {
                            console.log(e)
                            throw new Error(e.toString());
                        })
                );
            })
            .map((resp: boolean) => Actions[Types.LOAD_TAGS]());


    export const loadTagsEpic = (action$: ActionsObservable<Actions>): Observable<Actions> =>
        action$
            .ofType(Types.LOAD_TAGS)
            .mergeMap((action: ActionsType<Types.LOAD_TAGS>) => {
                return Observable.fromPromise(
                    axios.get("http://acostanza.com/tags-api/?key=secret_key&method=get")
                        .then(res => {
                            console.log(res);
                            return res.data;
                        })
                        .catch(e => {
                            console.log(e)
                            throw new Error(e.toString());
                        })
                );
            })
            .map((response: string[]) => Actions[Types.LOAD_TAGS_SUCCESS](response));

}

Wheew. That is a lot of code. Essentially we define an enum of action types, then a JavaScript object mapping those types to the implementation of the action creators. After that you see my definition of the State of the TagsContainer.

After the State definition, you can see the reducer that accepts the action and state and produces a new state through immutable transformations based on the action type. After the reducer you can see my Props definition transforming both the TagState and the actions I defined into actual props that are to be accepted by the connect method within the TagsContainer. Finally, you see the Epics definitions that handle side effects and call the API as well as call other actions when completed.

Sheesh… That’s a lot of stuff, but it’s easy enough to reason about if you read it enough times. As I mentioned, there would be a lot more boilerplate if I hadn’t made those helper types and methods. Now let’s see our full store, in Store.ts:

import { createStore, Store, combineReducers, applyMiddleware } from "redux";
import { TagStore } from './TagStore'
import { combineEpics, createEpicMiddleware } from 'redux-observable';

export interface AppState {
    health: TagStore.State;
}
/*
This is not necessary until there are multiple reducers and epics, 
but may as well add it early for this pattern
*/

const appEpic = combineEpics<any>(
    TagStore.persistTagsEpic,
    TagStore.loadTagsEpic
);
const epicMiddleware = createEpicMiddleware(appEpic);

const appState = combineReducers<AppState>({
    health: TagStore.store,
});

const w : any = window as any;
const composeEnhancers = w.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__;
export const store: Store<AppState> = createStore(
    appState,
    composeEnhancers(
        applyMiddleware(epicMiddleware)
    )
);

Technically it wasn’t necessary to combine reducers since I only have one, but I wanted to see what it’s really like to have multiple stores that could be combined into a single store via Redux, which is what I did here.

Finally, let’s see the TagsContainer.tsx which wraps the other components:

import * as React from "react";
import { connect } from 'react-redux';
import { TagStore } from '../../store/TagStore'
import { Tags, Props } from "../components/Tags";
import { TextTags } from "../components/TextTags";

import styled from "styled-components";
class TagsContainer extends React.Component<TagsContainerProps> {
    componentWillMount() {
        this.props.load();
    }
    persist = () => {
        console.log(this.props.selectedTags);
        this.props.persist(this.props.selectedTags);
    }
    render() {
        const selectedTagsProps: Props = {
            tags: this.props.selectedTags,
            selectEnabled: false,
            select: null,
            removeEnabled: true,
            remove: this.props.remove
        }

        const persistedTagsProps: Props = {
            tags: this.props.persistedTags,
            selectEnabled: true,
            select: this.props.select,
            removeEnabled: false,
            remove: null
        }
        return (
            <Wrapper>
                <SubWrapper>
                    Selected Tags:
                    <SubWrapper>
                <Tags {...selectedTagsProps} />
                </SubWrapper>
                    <button onClick={this.persist}>Save Tags</button>
                </SubWrapper>
                <SubWrapper>
                    <TextTags select={this.props.selectAll} />
                </SubWrapper>
                <SubWrapper>
                    Persisted Tags:
                <Tags {...persistedTagsProps} />
                </SubWrapper>
            </Wrapper>
        );
    }
}

const Wrapper = styled.div`
    display:flex;
    align-items:center;
    justify-content:center;
    width:500px;
    flex-wrap:wrap;
`
const SubWrapper = styled.div`
    display:flex;
    align-items:center;
    justify-content:center;
    width:100%;
    flex-wrap:wrap;
    margin:10px;
`

export interface TagsContainerProps extends TagStore.State, TagStore.Props { }

export default connect(
    TagStore.mapStateToProps,
    TagStore.mapDispatchToProps
)(TagsContainer);

The MobX Version

After spending many weeks trying to get the Redux types in a place I liked, I ended up coming across MobX. Rather than write too much about it, let’s see how I defined the TagsStore in MobX using TypeScript:

import { observable, action } from 'mobx';
import axios from 'axios';

export class TagStore {
    @observable persistedTags: string[] = [];
    @observable selectedTags: string[] = [];
    @observable error: string = "";

    //needs to be an arrow function otherwise will need to wrap in component
    //does not autobind if a regular function
    //for regular function could use @autobind
    @action selectTag = (tag: string) => {
        console.log(this.selectedTags);
        console.log(tag);
        this.selectedTags.push(tag);
        this.selectedTags = this.unique(this.selectedTags);
    }
    @action deselectTag = (tag: string) => {
        this.selectedTags = this.selectedTags
            .filter((o) => o !== tag);
    }

    @action selectTags = (tags: string[]) => {
        //could add the selectedTags to the list and uniqueify
        this.selectedTags = tags;
        this.selectedTags = this.unique(this.selectedTags);
    }

    @action persistTags = () => {
        axios.post("http://acostanza.com/tags-api/?key=secret_key&method=write", this.selectedTags)
            .then(res => {
                this.selectedTags = [];
                this.loadTags();
            }).catch(e => {
                console.log(e)
                throw new Error(e.toString());
            })
    }

    @action loadTags = () => {
        axios.get("http://acostanza.com/tags-api/?key=secret_key&method=get")
            .then(res => {
                this.persistedTags = res.data;
            })
            .catch(e => {
                console.log(e)
                throw new Error(e.toString());
            })
    }

    unique = (array: string[]) => {
        return array.filter((item, i, arr) => {
            return arr.indexOf(item) === i;
        })
    }
}

And well… that it. I’ve got to say, I was pretty amazed about how easy it was to just create a single POJO type object that had actions and then use that object to manage state through observables. Now let’s see what the TagsContainer looks like, it’s not much different:

import * as React from "react";
import { observer } from "mobx-react"
import { TagStore } from '../../store/TagStore'
import { Tags, Props } from "../components/Tags";
import { TextTags } from "../components/TextTags";

import styled from "styled-components";

@observer
export class TagsContainer extends React.Component<{ tagStore: TagStore }> {
    componentWillMount() {
        this.props.tagStore.loadTags();
    }

    render() {
        //need to dereference before passing to component
        //otherwise reference won't update
        const selectedTagsProps: Props = {
            tags: this.props.tagStore.selectedTags,
            selectEnabled: false,
            select: null,
            removeEnabled: true,
            remove: this.props.tagStore.deselectTag
        }

        const persistedTagsProps: Props = {
            tags: this.props.tagStore.persistedTags,
            selectEnabled: true,
            select: this.props.tagStore.selectTag,
            removeEnabled: false,
            remove: null
        }
        return (
            <Wrapper>
                <SubWrapper>
                    Selected Tags:
                    <SubWrapper>
                        <Tags {...selectedTagsProps} />
                    </SubWrapper>
                    <button onClick={this.props.tagStore.persistTags}>Save Tags</button>
                </SubWrapper>
                <SubWrapper>
                    <TextTags select={this.props.tagStore.selectTags} />
                </SubWrapper>
                <SubWrapper>
                    Persisted Tags:
                <Tags {...persistedTagsProps} />
                </SubWrapper>
            </Wrapper>
        );
    }
}

const Wrapper = styled.div`
    display:flex;
    align-items:center;
    justify-content:center;
    width:500px;
    flex-wrap:wrap;
`
const SubWrapper = styled.div`
    display:flex;
    align-items:center;
    justify-content:center;
    width:100%;
    flex-wrap:wrap;
    margin:10px;
`

You’ll note that in this case, I simply accept the TagStore as the props type and then use that store to access the selectedTags and persistedTags as well as call all of the same actions that I defined. In this particular case, I passed the tagStore explicitly through props, but just like with Redux I could easily provide the store through a Provider at the App level and then literally inject that store into any child component I desire without explicitly passing props down.

So What’s Better?

That’s really up to you. Do you prefer the functional approach to programming in which you have pure functions that work with immutable data structures or do you prefer more imperative programming with an object oriented approach? I’ve heard many people mention that Redux is better suited for big teams, which I think makes sense in part since you are using immutable data structures which makes it impossible to have mutated state in different parts of your application. However, I would make the argument that what I think is more important on a larger team would be defining certain paradigms that are required.

For instance, if using MobX always make an action in the store that must be called, do not mutate state directly from a component. I think that is pretty sensible. Similarly, even if you do mutate state from within a component, I’m not sure that really harms anything in the fact that all components that are observing that particular field will automatically be updated. I would still recommend using actions though to mutate state, rather than mutating it directly from within a component. Ultimately, it’s really up to you, the size of your team, and the general programming paradigms you prefer and would like to push forward as you maintain your app in the years to come.