Redux

Next we will setup redux to handle the state for our application (redux allows us to keep our components pure, helping testing and predictability).

You can think of redux as an implementation of the Flux pattern, where the main point is that data flows into a single direction.

Initialize

  1. This time we will only need to add the necessary dependencies to allow development with redux:
    yarn add redux redux-observable rxjs react-router-redux@next history
    
  2. Add the necessary type definitions (redux, redux-observable and rxjs contain type definitions):
    yarn add -D @types/react-router-redux @types/history
    

    Redux-observable is our choice for allowing side effects, such as AJAX-calls in redux. RxJS is a peer dependency for redux-observable. If you want something else you can check the alternatives. React-router-redux is used to tie navigation events and browser history into redux when using React Router (which well setup later), and history is needed to use react-router-redux.

Redux guards

We will begin by creating a file called guards.js inside the folder redux in src. This file will contain some helper functions so that TypeScript will play nicely with Redux. The contents of the file are as follows:

import { Action } from 'redux';

export type IActionType<X> = X & { __actionType: string };

const _devSet: { [key: string]: any } = {};

export const makeAction = <Z extends {}>(type: string, typePrefix = '') => {
    // Helpful check against copy-pasting duplicate type keys when creating
    // new actions.
    if (process.env.NODE_ENV === 'development') {
        if (_devSet[type]) {
            throw new Error(
                'Attempted creating an action with an existing type key. ' + 'This is almost cetainly an error.',
            );
        }
        _devSet[type] = type;
    }
    return <X extends (...args: any[]) => Z>(fn: X) => {
        const returnFn: IActionType<X> = ((...args: any[]) => ({ ...(fn as any)(...args), type })) as any;
        returnFn.__actionType = typePrefix + type;
        return returnFn;
    };
};

export const isAction = <T>(
    action: Action,
    actionCreator: IActionType<(...args: any[]) => T>,
): action is T & Action => {
    return action.type === actionCreator.__actionType;
};

Actions are the only way to send new content to the redux-state, and are usually in the form of an object with the properties type (a unique string) and an optional payload (something to pass to the reducer). However as redux has defined the type key to be of the type any, we lose type safety and that is why we alias the actual string to __actionType, which allows TypeScript to infer the type of an action implicitly, which is where makeAction comes in. The _devSet variable and the things related to it inside makeAction are for development, to ensure we don't create duplicate actions. The isAction-function is a type guard which allows us to use the action creators (more about them in reducers) own return type as the actual type for the action, giving us implicit, but safe, typings. You can read more about redux guards here. Don't worry if this seems too complex as it uses a lot of advanced features of TypeScript to work.

Reducer

Now we will define our root-reducer in a file called reducer.ts inside the folder redux:

import { combineReducers } from 'redux';
import { combineEpics } from 'redux-observable';
import { routerReducer, RouterState } from 'react-router-redux';

const reducer = combineReducers<State>({
    router: routerReducer,
});

export class State {
    readonly router: RouterState = null;
}

export const epics = combineEpics(
);

export default reducer;

This file will allow us to export all of the following:

  • Our root reducer (all specific reducers will be combined into this one, as according to redux documentation, allowing our reducers to only handle a slice of the entire state) made with combineReducers
  • The class of our entire state (defined as a class to allow initialization in for example tests)
  • Our combined epics (more about epics later) made with combineEpics

Store

Now we will define our store creator. Having it as a separate function helps us in doing server-side rendering but if you don't want to do it you can define this function later. The store creator goes in a file called store.ts inside the redux-folder:

import { createStore, applyMiddleware } from 'redux';
import { createEpicMiddleware } from 'redux-observable';
import { History } from 'history';
import { routerMiddleware } from 'react-router-redux';
import reducer, { epics, State } from './reducer';

const epicMiddleware = createEpicMiddleware(epics);

const configureStore = (history: History) => createStore<State>(
    reducer,
    applyMiddleware(routerMiddleware(history), epicMiddleware),
);

export default configureStore;

On the fifth line we create a middleware for our store to handle our epics, using createEpicMiddleware (and here you see why we combined all our epics into one):

import { createEpicMiddleware } from 'redux-observable';
import { epics } from './reducer';
const epicMiddleware = createEpicMiddleware(epics);

And then on the seventh line we define our store creator method (which is exported on the 14th line):

import { createStore, applyMiddleware, Store } from 'redux';
import { History } from 'history';
import { routerMiddleware } from 'react-router-redux';
import reducer, { State } from './reducer';
const configureStore = (history: History) => createStore<State>(
    reducer,
    applyMiddleware(routerMiddleware(history), epicMiddleware),
);
export default configureStore;

createStore is the function that creates a Store for redux and as it's first argument it takes the root-reducer and as the second one all the applicable middleware (combined with applyMiddleware), in this case our epicMiddleware and routerMiddleware. The function configureStore takes a History as an argument, to allow us to call it with different types of histories.


Now we have everything set up to start doing the beef of the application, a.k.a. the views!

Alternatives

If redux doesn't float your boat, you can always try MobX, but redux is maybe the more used one at this point.

For redux-observable you have multiple alternatives, namely:

results matching ""

    No results matching ""