React Redux with Typescript

Redux is a perfect candidate for Typescript. As an app gets larger, with more actions and more complex reducers, it is all too easy to lose the structure of the root state and to pass inappropriate data through action payloads. Typescript adds a compile time guard against falling into reducer hell.

Unfortunately, to use typescript straight out of the box with Redux, all the reducers and actions must be explicitly given types that agree with each other. All this type duplication can just introduce a new place for error and discrepancy.

In a recent project, our team developed a set of utility functions and complementary types and interfaces that allow for strong typing in Redux with a minimum of type and interface duplication.

The pattern has the following benefits:

  • Strongly Typed Redux - No more throwing around the wrong payload.
  • Only define an action payload's type once - The type is then inferred in all interested reducers, action creators, and calling components.
  • Defined Root State - Strongly typed state with easily referenceable structure.

To illustrate the pattern and how to use it, I'll walk through a trivial Redux app that implements strong typing. I'll assume familiarity with React/Redux architecture as well as a webpack build system, but if you'd like a introduction to that, check out this post.

If, instead, you'd prefer to skip my prosaic meanderings, you can check out the example code here on github.

Our app will be a trivial masterpiece consisting of two components, user_buttons and flash_message. The former will render two buttons that allow the user to dispatch an action and flash_message will subscribe to the state and display the content of a fairly worthless message.

This walkthrough will follow this outline.

  • File Structure
  • Typescript Setup
  • Actions
  • Reducers
  • Wiring Components

Throughout, I'll introduce how to implement a variety of utility functions that will allow our application to easily make use of Typescript's goodness. I'll focus on the essential helper functions, but will omit some code sections and interfaces for brevity. Check the example code repo to fill in any holes.

Project Structure

Our base directory structure is as follows:

├── actions
├── app
├── interfaces
├── reducers
└── utils

The actions and reducers directories will contain their eponymous redux logic. The app directory holds our components and interfaces has all interfaces required through our application. The utils folder will house the specific functions and interfaces that our team developed to allow our project to easily make use of strong typing. I'll go into more detail of the file structure in the following sections.

Typescript Setup

Setting up Typescript with your React project consists of two steps: adding the correct loader to Webpack and downloading typings files for third party libraries.

Webpack

To set up Typescript with Webpack, make use of the ts-loader npm package for all .ts and .tsx files. Then, add in a tsconfig.json to manage your Typescript settings. I also made use of the webpack-dev-server to easily run our app. Checkout the code repo for an example.

Typings

Conveniently enough, there are a number of high quality type declaration files available so that we can use third party modules with Typescript without having to write the damn things ourselves. Typings is a package similar to npm that allows us to manage our type declaration files. Install typings as a dev-dependency, and then run the rather unfriendly typings install dt~<libary name> --global --save. This will grab the typings file for you, and save it to typings.json (a package.json equivalent). Grab the typings files for all the external libraries and typings will make their interfaces globally available in your project.

Actions

In our project, we named action files by the section of the application they manipulate. Our action directory for this example will look like this:

actions  
└── user_buttons
    ├── action_types.ts
    └── actions.ts

In our case, only user_buttons is concerned with dispatching actions, but this pattern would be repeated for other sections in larger apps. We'll begin by creating a map of our action constants strings in action_types.ts. By making use of the createActionConstantsMap and actionType<T> helper functions, we'll be able to create a map of action constants that have properly typed payloads. This is the core of the strong typing around our Redux state and its actions.

I'll illustrate the use of both helper functions, and then show what they're doing under the hood.

Our app will have one action, SEND_FLASH_MESSAGE which will have a payload of type FlashMessage. The FlashMessage type will be an object with message and className properties.

// action_types.ts
import { FlashMessage } from '../../interfaces/flash_message.interface';  
import { actionType, createActionConstantsMap } from '../../utils/redux';

export default createActionConstantsMap('userButtons', {  
  SEND_FLASH_MESSAGE: actionType<FlashMessage>()
});

The actionType<T> function does the necessary type casting for us. When we pass the action map in action_types.ts to createActionConstantsMap, we need to pass our desired payload structure as the generic argument to actionType<T>. Here's what actionType is doing in the background.

// reduxTypes.ts

export function actionType<TPayload>() {  
  return null as ActionType<TPayload>;
}

This function will cast the value of our action map at that specific key (in our case SEND_FLASH_MESSAGE) as an ActionType<TPayload> type with the given payload interface regardless of the value. The function will return null, but a properly cast null value for each action key.

The createActionConstantsMap function will take care of creating an actual string value constant to be passed around and referenced by the reducers. As it is, the object we pass into createActionConstantsMap in action_types.ts, has a key value SEND_FLASH_MESSAGE and a value of null typed as an ActionType<FlashMessage>. Null isn't particularly useful for distinguishing actions inside the reducers, so we'll generate a name-spaced string for each key within the createActionConstantsMap function.

// actionUtils.ts

export function createActionConstantsMap<T extends ActionTypeMap>(prefix: string, input: T): T {  
  prefix = prefix.toUpperCase() + '_';

  const map = Object.keys(input).reduce((acc, key) => {
    const namespacedActionType = prefix + key;
    return Object.assign(acc, { [key]: namespacedActionType });
  }, {}) as T;
  return map;
}

The typing comes in through a combination of type inference and type casting inside this function. When we pass our input argument, it has a structure as follows:

{
  SEND_FLASH_MESSAGE: null, // Cast as ActionType<FlashMessage>
}

This type is maintained inside createActionConstantsMap as the generic type T which is inferred by Typescript. Next, we generate an new object with the same key values, but name-spaced strings, and then cast that object as type T. This is the key point from whence the typing magic flows. When we reference the SEND_FLASH_MESSAGE property on the action types map, it has the proper payload interface bound to it.

Reducers

We'll write out a single reducer that will manipulate the application state based on our SEND_FLASH_MESSAGE action.

We placed all of our reducer logic inside a single reducers directory split up by roughly by component. Our directory structure will look as follows:

reducers  
└── flash_message
    ├── flash_message_reducer.ts
    ├── flash_message_selectors.ts
    ├── flash_message_state.interface.ts
    └── index.ts

I'll walk through the structure and responsibility of each file in turn.

Reducer Interface

We'll first define the structure of the subsection of the root state the flash message reducer deals with.

// flash_message_state.interface.ts

import { FlashMessage } from '../../interfaces/flash_message.interface';

export interface FlashMessageState {  
    flashMessage: FlashMessage;
}

These subsections of the root state will be defined for each reducer section and collated into a single RootState interface.

Next, we'll create our reducer making use of some helper functions to maintain strong typing.

Reducer Logic
// flash_message_reducer.ts

import { FlashMessageState } from './flash_message_state.interface';  
import flashMessageActionTypes from '../../actions/user_buttons/action_types';  
import { createActionHandler, createReducer } from '../../utils/redux';

const handleAction = createActionHandler<FlashMessageState>();

const handleFlashMessageSent = handleAction(flashMessageActionTypes.SEND_FLASH_MESSAGE, (state, action) => {  
    const { message, color } = action.payload;
    return Object.assign({}, state, {
        flashMessage: {
            message,
            color
        }
    });
});

const initialState: FlashMessageState = {flashMessage: {message: '', color: ''}};

export const flashMessageReducer = createReducer(initialState, [  
    handleFlashMessageSent
]);

The first thing we need to do is create a function handleAction through the createActionHandler helper function. createActionHandler will bind a generic type as the state that the reducer interacts with, in this case FlashMessageState.

// actionUtils.ts
...
export function createActionHandler<TState>() {  
  return <TPayload>(actionType: ActionType<TPayload>, handler: ActionHandler<TState, TPayload>): TypedActionHandler<TState, TPayload> => {
    return Object.assign(handler, { actionType: actionType.toString() });
  };
}

The handleAction function that is returned, takes the action type we're listening for, imported from our map created earlier, and the reducer handler function which contains the reducer logic. handleAction will return a new object with a handler property corresponding to the handler function and an actionType property which is the string constant to trigger that reducer logic.

This function is essential for maintaining the proper typing for the both the state and the action payload. The handler function is now properly bound to the state type passed in through the createActionHandler function, and the payload type it can expect through the action type argument. In our case, <TState> is bound to FlashMessageState type after invoking createActionHandler, and <TPayload> is inferred as <FlashMessage> after passing in flashMessageActionTypes.SEND_FLASH_MESSAGE.

Once we have our reducer function, handleFlashMessageSent, we will pass it to our createReducer utility function.

In addition to handling initial state, createReducer will create a closure around an object variable mapping each of the passed handler's actionType property as a key and each handler function as the value.

// reducerUtils.ts

import { Action, Reducer, TypedActionHandler, TypedActionHandlerMap } from './';

interface GetInitialState<T> {  
    (): T;
}

export function createReducer<TState>(initialState: TState|GetInitialState<TState>, handlers: TypedActionHandler<TState, any>[]): Reducer<TState> {

  const handlerMap = handlers.reduce((acc, handler) => {
    return Object.assign(acc, { [handler.actionType]: handler });
  }, {} as TypedActionHandlerMap<TState>);

  const getInitialState = typeof initialState === 'function' ? initialState : () => initialState;

  return (state: TState, action: Action<any>) => {
    if (typeof state === 'undefined') {
      state = getInitialState();
    }

    const handler = handlerMap[action.type];
    return handler ? handler(state, action) : state;
  };
}

The function createReducer returns, then references the passed in action type and invokes the given handler passing in the current state. All handlers will be composed using this function as a single reducer, and they will in turn be exported and collated into the root reducer.

Reducer Selectors

For each reducer, we'll create a set of selectors which we can use to get access to specific data on the state.

// flash_message_selectors.ts

import { createStateSelector } from '../../utils/redux';

export const getFlashMessage = createStateSelector(state => state.flashMessage);  

We'll import the createStateSelector helper function which will use the reselect library and add some typings.

// selectorUtils.ts

import { createSelector } from 'reselect';  
import { SelectorWithoutProps } from './reduxTypes';

export function createStateSelector<TResults>(selector: SelectorWithoutProps<TResults>): SelectorWithoutProps<TResults> {  
  return selector;
}
CreateStore

Once we have all our reducers set up, we'll import them into a createStore function which will combine them together along with our root state and any middleware, and create a store which we can pass to our Provider.

// createStore.ts

import * as Redux from 'redux';  
import thunk from 'redux-thunk';  
import { Dispatch, RootState } from '../utils/redux';  
import { flashMessageReducer, stateKey as flashMessageStateKey } from '../reducers/flash_message';

const rootReducer = Redux.combineReducers<RootState>({  
    [flashMessageStateKey]: flashMessageReducer
});

export interface Store extends Redux.Store<RootState>{  
    dispatch: Dispatch;
};

export default (initialState = {}) => {  
    const middleWare = Redux.applyMiddleware(thunk);
    return Redux.createStore<RootState>(rootReducer, initialState as RootState, middleWare);
};

createStore will bring in all reducers, in this case just the flashMessageReducer, and map them by a stateKey value which is exported from each reducer's index.ts file. createStore will also reference the RootState interface where we'll define the structure of our application state. Each key in the Root State object should map to the stateKey value for each reducer. In our example, the state key for the flash message reducer is the string 'flashMessage' the same key on Root State that this reducer is responsible for. As more reducers and pieces of state are added, the Root State will expand. This will give us a single source of truth to reference the structure of our application state.

// rootState.ts

import { FlashMessageState } from '../../reducers/flash_message';

export interface RootState {  
    flashMessage: FlashMessageState;
};

Wiring Up Components

Linking typed state and action creators to the components works very much the same way as it does in plain old javascript Redux. Of our two components flash_message.tsx will be concerned with displaying a state property, and user_buttons.tsx will dispatch our action.

The third party typings files available for React/Redux do most of the Typescript work for us. We need to pass component property interfaces into two places. The first place, when we declare the component, we'll need to pass in an intersection type of a composition of the component's own props, the Redux state props it listens to, and the Redux dispatch props. The second place we need to pass interfaces in is the connect Redux function. connect takes in three generic type arguments, connect<TStateProps, TDispatchProps, TOwnProps>.

Out of the box, we would need to define all these interfaces to mirror the types returned from our mapStateToProps and mapDispatchToProps functions. We can save ourselves some work by using some Typescript type inferencing magic. The returnType helper function, when given a function as an argument, will return a null value given the same type of the passed function's return type.

// typeUtils.ts

interface Func<T> {  
    ([...args]: any): T;
}

export function returnType<T>(func: Func<T>) {  
    return null as T;
}

We can then use Typescripts obscurely named typeof operator to access the type of the null value. In our example, we'll pass in the mapStateToProps and mapDispatchToProps functions to returnType which will automatically infer the type of the Redux state our component is listening to.

In practice, this is what our flash_message component will look like.

// flash_message.tsx

import * as React from 'react';  
import { connect } from 'react-redux';  
import { getFlashMessage } from '../../reducers/flash_message';  
import { bindActionCreators, RootState, Dispatch, returnType } from '../../utils/redux';

function mapStateToProps(state: RootState) {  
    return {
        flashMessage: getFlashMessage(state)
    };
}

interface FlashMessageOwnProps {  
}

const stateGeneric = returnType(mapStateToProps);  
type FlashMessageStateProps = typeof stateGeneric;  
type FlashMessageProps = FlashMessageStateProps & FlashMessageOwnProps;

class FlashMessage extends React.Component<FlashMessageProps, {}> { 

    render() {
        const { color, message} = this.props.flashMessage.flashMessage;

        return (
            <p style={{color}}>{message}</p>
        );
    }
}

export default connect<FlashMessageStateProps, {}, FlashMessageOwnProps>(mapStateToProps)(FlashMessage);  

For the mapStateToProps function, we'll use a selector function we defined in our reducer directory.

By using returnType and Typescript's typeof, we can inference the return type of mapStateToProps and can appropriately type our component's props. The user_buttons component works much the same way except we're concerned with the mapDispatchToProps function instead.

At this point, we have all the typed Redux wired up for our trivial little application. While Typescript has some initial overhead to get over, we've found it a huge help once our components get more complex. It gives us a compile time guard against passing in woefully inappropriate data into our action creators and allows us to know exactly what our reducers are going to be dealing with. With a few essential patterns and utility functions, we can greatly reduce the amount of duplication necessary to keep those actions reducing smoothly.