Type-Checking React and Redux (+Thunk) With Flow - Part 2

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
view raw flow-typed hosted with ❤ by GitHub

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
<strong>e</strong>

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;
}
}
<strong>type</strong> State = number;
view raw gistfile1.js hosted with ❤ by GitHub

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'
view raw type ActionType hosted with ❤ by GitHub

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 Actiontypes.

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.

code action creator

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

code 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.

code annotate mapStateToProps with the State

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 React and Redux project.

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?

Insights

Learn more about Developer Experience

cover

May 5Article

Codex Ambassador: Q1 Recap Across 5 Cities

A short recap of Codex events in Berlin, Wroclaw, Warsaw, Sopot, and Miami, plus what we learned from each format and where we want to go next.

cover

Feb 5Live Stream

Storybook Explained: From Basics to Advanced Workflows

Core Storybook concepts, stories and args, interaction testing, and how Storybook works in React Native projects.

cover

Jan 29Article

Announcing AI SDK Profiler for React Native

Announcing the AI SDK Profiler plugin for Rozenite DevTools. Capture OpenTelemetry spans from the Vercel AI SDK and inspect AI requests across on-device and cloud providers.

cover

Nov 28Talk

How I Hacked React Native DevTools (And Built a Plugin Framework)

Learn how React Native DevTools work under the hood, and how to inject custom plugins with clean hooks and no forks.

cover

Sep 16Talk

How I Hacked React Native DevTools (And Built a Plugin Framework)

Learn how React Native DevTools work under the hood, and how to inject custom plugins with clean hooks and no forks.

cover

Jul 28Article

Introducing Rozenite: A Plugin Framework for React Native DevTools

Build custom debugging panels inside React Native DevTools with Rozenite: a modular, TypeScript-ready plugin framework with full device-scoped support.

cover

Feb 12Article

How to Easily Create Native Libraries With Nitro and Turbo Modules

Learn how to optimize React Native performance with Turbo and Nitro Modules. Ditch the legacy bridge and master modern native module development.

cover

Dec 4Podcast

Radon IDE: Better Developer Experience for React Native Teams

Find out how Radon IDE can help increase team velocity and rid you of the common React Native app development pain points.

cover

Sep 19Podcast

Exploring Bun: Jarred Sumner on Hyperfast JavaScript Toolkit

Is Bun the future of React Native package management? Join us as we talk with Jarred Sumner, the creator of this lightning-fast JavaScript toolkit.

cover

Sep 11Podcast

Vim: from Zero to Hero

Discover the power of Vim, the legendary text editor, as we explore its origins, standout features, and practical tips to boost your coding efficiency.

cover

May 15Article

Add-Ons for Web Browsers With React

At times you may need to create an add-on to use a web browser more effectively. Find out how to build one with React.

cover

Jan 27Article

Why Is It Worth to Automate Your Dependency Management with `Autolinking`?

When you automate your dependency management, you can fully focus on developing your app. Find out how you can do it with autolinking.

cover

Mar 19Tutorial

The Hard Way vs. The React Native AI SDK Way: Stop Writing Custom Modules for Every Model

Learn how React Native AI uses a unified provider model to switch between cloud and on-device LLMs without rewriting application logic, enabling reliable AI experiences online and offline.

cover

Mar 12Tutorial

Build Smarter Apps: Tool Calling & AI Orchestration Explained

Learn how on-device models in React Native can call tools, interact with external APIs, and return structured outputs you can use directly in application logic.

cover

Mar 5Tutorial

MLC LLM + React Native: On-Device AI Without the Pain

Learn how to run third-party on-device LLMs in React Native using MLC LLM. Choose your models, run them efficiently across platforms, and keep a unified JavaScript API with React Native AI.

cover

Feb 24Tutorial

Intelligent Fallbacks with Apple Intelligence in React Native

Learn how to use Apple’s built-in Foundation Models in React Native apps, reduce memory usage, and enable fast, fully on-device AI with React Native AI Apple.

cover

Feb 10Tutorial

What Is the React Native AI SDK? A Complete Intro & Quickstart

Learn why on-device AI matters for React Native apps, how local LLMs behave offline, and what problems React Native AI is designed to solve from day one.

cover

Jan 29Tutorial

Implementing an Android TurboModule with Kotlin

Learn how to implement a React Native TurboModule on Android, from the TypeScript spec and Codegen to the Kotlin implementation and package registration.

cover

Jan 19Tutorial

Working with Different Threads in Swift TurboModules

Learn how JavaScript, native module, background, and main threads interact in React Native TurboModules, and how to use GCD queues to run async work safely in Swift.

cover

Jan 9Tutorial

How to Add Type-Safe Constants to Swift TurboModules

Learn how to expose typed, immutable constants from a Swift TurboModule using Codegen, and get full end-to-end type safety between native and JavaScript.

cover

Dec 30Tutorial

Adding Event Emitters to Your TurboModule in Swift

Learn how to emit native events from a Swift TurboModule and subscribe to them in JavaScript, including proper memory management and Codegen integration.

cover

Dec 22Tutorial

Writing Your First TurboModule in Swift

Learn how to build a fully working TurboModule in Swift, integrate it with Codegen, and bridge it through Objective‑C inside an Expo app.