One of the most popular examples on using Flowtype with Redux is the F8 app, open-sourced by Meta last year.

It deeply integrates with Flow, most notably:

  • It replaces constants in favor of a single union type Action that describes the type and payload of every action that can be dispatched,
  • Later in reducers, typed Action helps developers to reason about the purpose of action and what could be done with its payload,
  • Each branch of the state has a corresponding type that helps with transforming it.

I have been using similar approach in most of my apps recently. However, I quickly noticed that some of these do not scale well as the app grows, especially when there are several developers working on the app. The setup I want to share with you today is a result of the tweaks and improvements that we added over time to the original F8 App approach. I hope you’ll find it useful!

#1 — Keep types in a single file

Our codebases tend to grow quickly as we add new reducers, actions and selectors. Having types spread across multiple files makes it harder to see them all at a glance and import to your components. It also creates a lot of cross references between files when you need to build a type that composes others inside.

In most of our apps, we tend to keep all the types in a single file. Usually we call it types.js and place it in the root folder. Types that it contains are just named exports, like below:

/**
* @flow
*/
view raw gistfile1.txt hosted with ❤ by GitHub
export type Dispatch = Function;
view raw gistfile1.txt hosted with ❤ by GitHub

that can be later imported in a very easy way:

import type { Dispatch } from '../types';
view raw gistfile1.txt hosted with ❤ by GitHub

#2 — Describe your state

Although we promote keeping reducers small and atomic, they still contain the logic necessary to change the app state in a response to an action. Things get tricky when we fetch details from the API. We need to describe initial state and the lack of value as the request is pending.

Take the following example of two reducers’ state:

type Friend = {
name: string,
};
view raw gistfile1.txt hosted with ❤ by GitHub
type FriendsState = {
list: Array<Friend>,
loading: boolean,
};
view raw gistfile1.txt hosted with ❤ by GitHub
type AppState = {
isMenuOpen: boolean,
};
view raw gistfile1.txt hosted with ❤ by GitHub

It has flow types for the friends.js and app.js reducers. All good — now we can use them inside our reducers like below:

/**
* @flow
*/
import type { FriendState } from '../types';
type State = FriendsState;
const initialState = {
loading: false,
list: null,
};
export function friends(
state: State = initialState,
action: Object
): State {
return state;
}
view raw gistfile1.txt hosted with ❤ by GitHub

Did you actually notice that the above code has a type error?

src/reducers/friends.js:9
9: list: null,
^^^ null. This type is incompatible with
2: list: Array<{ name: string }>,
^^^^^^^^^^^^^^^^^^^^^ array type. See: src/types.js:2
view raw gistfile1.txt hosted with ❤ by GitHub

We declared that the friends.list in state will always be an array, either empty or containing some fetched details. However, in our initialState, it’s default value is null. That would potentially cause an exception, since it changes the contract that was settled at the beginning.

Thanks to Flow and the state having a corresponding type, we were able to catch that tiny mistake as early as it happened. It’s these trivial mistakes that can potentially turn our app into an inconsistent or broken state. That’s why it is important to describe the state controlled by a reducer, so that you can catch these things as early as possible.

#3 — Connect like a pro

When working with Redux, we use connect to map state to props of our containers and let them react to changes when they happen.

A simple example could be:

export default connect(
(state) => ({
list: state.friends.list,
})
)(Container);
view raw gistfile1.txt hosted with ❤ by GitHub

How many times writing something similar you had to step back, check reducers and see the state shape? I guess a lot.

Now, since each of our reducers has its own type, like FriendsState as we saw in the previous paragraph, we can easily introduce yet another one, called State:

type State = {
friends: FriendsState,
app: AppState,
};
view raw gistfile1.txt hosted with ❤ by GitHub

and use it in our connect:

/**
* @flow
*/
import { connect } from 'react-redux';
import type { State } from '../types';
export default connect(
(state: State) => ({
list: state.friends.list,
})
)(Container);
view raw gistfile1.txt hosted with ❤ by GitHub

This pattern helps us to reason about the entire app state as well as eliminate common issues, like misspelling the property names. Thanks to that, we can catch them early, as we type, instead of debugging it during runtime.

#4 — Be careful with typing actions

Following the F8 app approach, we describe our actions as a single, union type, called Action:

type Action =
| { key: 'LOAD_FRIENDS' }
| { key: 'LOAD_FRIENDS_SUCCESS', payload: { list: Array<Object> } }
view raw gistfile1.txt hosted with ❤ by GitHub

However, union type holding information about all the actions, in the form as above, was something that stopped working for us as the app grew.

The main problem was that we were using redux-promise-middleware to automatically dispatch success and failure case as the promise was either resolved or rejected.

What we really wanted is to have each action, especially the one that’s a result of an API call, to be fully typed, so that it’s easier to map response body of an HTTP request to the state. At the same time, we didn’t want to repeat things like key and payload every time, since effectively the only thing that’s changing is the response body itself.

So, instead of focusing on typing actions, we decided to create one generic type, called ApiAction:

type ApiAction<T> = {
key: string,
payload: T,
};
view raw gistfile1.txt hosted with ❤ by GitHub

that can be used inside reducers to map an action with the payload.

Thanks to that, we could focus on typing payloads instead, as in this example:

type Friends = Array<Friend>
view raw gistfile1.txt hosted with ❤ by GitHub

Note: We already have a Friend and Friends type that we are using inside FriendsState, so it’s less typing for the same security.

Finally, implementing it in our reducer is as simple as this:

/**
* @flow
*/
import type {
Friends,
Friend,
ApiAction,
Handler,
FriendsState,
} from '../types';
const initialState = {
loading: false,
list: null,
};
const handlers: Handler<FriendsState> = {
['LOAD_FRIENDS_SUCCESS'](state, action: ApiAction<Friends>) {
// here, action is typed with response
return s;
},
};
export createReducer(initialState, handlers);
view raw gistfile1.txt hosted with ❤ by GitHub

In this example, we define handlers as an object where each key is a type of an action to handle and function is a transform to be applied to the state. It can be described with the following Flow type:

type Handler<T> = {
[key: string]: (state: T, action: Action<*>): T
};
view raw gistfile1.txt hosted with ❤ by GitHub

If you are interested in an implementation of createReducer, check this gist!

#5 — Use constants or not

Last important concept are constants. I haven’t payed too much attention to them just yet, mainly because they are not the main bottleneck inside the app, as it grows.

The focus we had was always on ensuring that it’s as easy for the developer to work with the action payload inside reducer rather than checking whether the action key is correct.

Having said so, up to this point, we tend to define constants in a separate file the old school way, however sometimes we define an enum, called ActionType that can be used in all the above types we mentioned as a replacement to string:

type ActionType =
| 'LOAD_FRIENDS'
| 'LOAD_FRIENDS_SUCCESS'
| 'LOAD_FRIENDS_FAILURE';
view raw gistfile1.txt hosted with ❤ by GitHub

Flow will warn us in case of a spelling mistake.

Wrapping up

There’s definitely a lot of fun when it comes to typing Redux with Flow. The amount of possible solutions, especially with actions, constants and reducers opens up a room for a lot of discussion and improvements. As always in this case, pick the solution that works for you and gives you the most benefits in that very project you are working. I hope the tips that I shared today will give you yet another point of view on using Flow with Redux!

Thanks to Max Stoiber and Nader Dabit for making sure it can be understood by more than just me.

Discover services offered by our React Native development company.

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.