Type-checking React and Redux (+Thunk) with Flow — Part 2

Satyajit Sahoo
Callstack Engineers
7 min readMar 6, 2017

This is the second part of a two part series and only covers Redux and Redux Thunk. If you want to read about type-checking React, see Part 1.

So you’ve setup Flow in your React project and ready to go. But your application is growing, and you want to add Redux for state management. Now, how do you make your redux code type-check with Flow?

In this article, we’ll cover how to type-check Redux. We have very simple counter app which we’re going to type-check. We are also using the thunk middleware for Redux. Things may vary when you have a more complex project or use different middlewares.

Mike Grabowski wrote an article a while back covering some part of it. Make sure to check it out. We’ll expand a bit more in this article and try to cover all the steps you need to do.

Keep in mind that this is the result of learning and experimenting. You might need to tweak it for your workflow and taste.

Install Type Definitions

The first thing we need to do is to install the type definitions for Redux and and React Redux. Thanks to flow-typed, they are just one command away.

Install flow-typed if you haven’t done that yet, then cd to your project directory and type:

flow-typed install

This will automatically find and install type definitions for Redux, React Redux and other libraries specified in your package.json.

Make sure you’ve installed flow-bin locally from NPM first.

Type-check Actions

We’ll declare types for all actions in a your project and export a single Action type. Declaring types for all the actions might seem a little bit of work, and you might be tempted to define a generic type like { type: string, payload?: any }, but declaring specific types will help to find mistakes both in your action type strings and shape of payload.

type IncrementAction = {
type: 'INCREMENT_COUNTER',
payload: number,
}
type DecrementAction = {
type: 'DECREMENT_COUNTER',
payload: number,
}
export type Action =
| IncrementAction
| DecrementAction

Note that it also depends on what other libraries you use. If you use libraries which generate actions for you, then this list can get out of date quickly. If the library always generates a specific shape of action, you can probably define a generic type for it.

Type-check Reducers

We can define the reducer normally with type annotations and it’ll mostly work:

import type { Action } from '../types/Action';type State = number;export default function counter(state: State = 0, action: Action) {
switch (action.type) {
case 'INCREMENT_COUNTER':
return state + action.payload;
case 'DECREMENT_COUNTER':
return state - action.payload;
default:
return state;
}
}

This will have basic type safety, but still fragile, because flow can infer the type of the action from action.type, but if you make a typo in action.type, then it’ll infer the type as any, basically bypassing type check.

To avoid this scenario, we can use a helper similar to what’s described in Redux docs:

import createReducer from '../lib/createReducer';
import type { Action } from '../types/Action';
type State = number;const initialState = 0;export default createReducer(initialState, {
INCREMENT_COUNTER: (state: State, action: Action) =>
state + action.payload,
DECREMENT_COUNTER: (state: State, action: Action) =>
state + action.payload,
});

Our helper can look like this:

import type { Action } from '../types/Action';type Reducer<S, A: Action> = (S, A) => S;export default function createReducer<S, A: *>(
initialState: S,
handlers: { [key: string]: Reducer<S, A> }
): Reducer<S, A> {
return function reducer(state: S = initialState, action: A): S {
return handlers.hasOwnProperty(action.type)
? handlers[action.type](state, action)
: state;
};
}

Note that this doesn’t prevent you from typos in action names. But unlike switch statements, trying to access non-existing properties on the action object gives a warning since actions will no longer be inferred. In addition, flow can no longer refine type of the action according to the action type, so it has a different set of tradeoffs.

You could also define a ActionType enum with all the actions and replace [key: string] in above helper with [key: ActionType] to catch typos.

type ActionType =
| 'INCREMENT_COUNTER'
| 'DECREMENT_COUNTER'

Though it’s repetitive given that we already have an enum with all possible actions.

Type-check State

To make it easier to define the type for the redux state, we need to do some changes in the root reducer. Basically, move the reducer map to a separate object and export the Reducers type.

import { combineReducers } from 'redux';
import counter from './counter';
const reducers = {
counter,
};
export type Reducers = typeof reducers;export default combineReducers(reducers);

Thanks to this great tip from Adam Miskiewicz, we can automatically generate the types for our state from our reducers.

import type { Reducers } from '../reducers';type $ExtractFunctionReturn = <V>(v: (...args: any) => V) => V;export type State = $ObjMap<Reducers, $ExtractFunctionReturn>;

Isn’t it magic? Yes, yes, it is.

What I love about this approach is, your reducers describe your state, both for Redux and for Flow. You don’t have to type out the shape of your state separately for flow, which is great because it will never get out of date.

Type-check Store

To define types for the store, we can import the Store and Dispatch from the installed definitions, and then set them up with our State and Action types.

Since we’re using the redux-thunk middleware, our Dispatch method will be little different than the default method, i.e. — we need to support dispatching thunks along with plain actions.

import type {
Store as ReduxStore,
Dispatch as ReduxDispatch,
} from 'redux';
import type { Action } from './Action';
import type { State } from './State';
export type Store = ReduxStore<State, Action>;export type GetState = () => State;export type Dispatch =
& ReduxDispatch<Action>
& Thunk<Action>
export type Thunk<A> = ((Dispatch, GetState) => Promise<void> | void) => A;

Now we just need to annotate the store with our shiny new type:

import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from '../reducers';
import type { Store } from '../types/Store';
const enhancer = compose(
applyMiddleware(thunk),
);
export default function configureStore(): Store {
return createStore(rootReducer, enhancer);
}

Type-check Action Creators

Action creators are just simple functions, so we just need to annotate them with the types we created before:

export function increment(amount: number = 1): Action {
return {
type: 'INCREMENT_COUNTER',
payload: amount,
};
}
export function decrement(amount: number = 1): Action {
return {
type: 'DECREMENT_COUNTER',
payload: amount,
};
}
export function incrementIfEven() {
return (dispatch: Dispatch, getState: GetState) => {
const { counter } = getState();
if (counter % 2 === 0) {
dispatch(increment());
}
};
}

Flow will warn when you’re returning invalid actions from the action creator, or trying to dispatch an invalid action.

We don’t necessarily have to use action creators. Since we’ve typed our store above, we could easily dispatch plain actions.

Type-check Containers

The last bit of the puzzle is type-checking the container components. We just need to annotate mapStateToProps with the State type we created earlier.

import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import Counter from '../components/Counter';
import { increment, decrement, incrementIfEven } from '../actions/CounterActions';
import type { State } from '../types/State';
const mapStateToProps = ({ counter }: State) => ({
counter,
});
const mapDispatchToProps = (dispatch: *) => bindActionCreators({
increment,
decrement,
incrementIfEven,
}, dispatch);
export default connect(
mapStateToProps,
mapDispatchToProps,
)(Counter);

Combine with the react-redux type definitions we installed earlier, it gives proper type-checking for the props we’re passing to the underlying component.

For example, if we forget to pass a prop, or pass a prop of invalid type in mapStateToProps or mapDispatchToProps other than what’s specified in the Counter component, Flow will warn us.

Wrapping up

That’s it. We now have a decent type-checked React and Redux codebase. You can check the complete project here — satya164/react-boilerplate

There’s so much more to learn and experiment, but I hope this gave you a good starting point. Do you have any tips on how to improve this setup?

This article was originally published at callstack.com on March 6, 2017.

--

--

Satyajit Sahoo
Callstack Engineers

Front-end developer. React Native Core Contributor. Codes JavaScript at night. Crazy for Tacos. Comic book fanatic. DC fan. Introvert. Works at @Callstackio