Flow: Flow and constants in Redux

Created on 29 Oct 2018  路  13Comments  路  Source: facebook/flow

In Redux we should use constants, not strings. Using strings is stupid (by Dan Abramov). But in docs you shown how to use Flow + Redux with strings, not constants. Unfortunately, there is no any good approach how to combine Flow and Redux with constants, not strings. How to solve that problem?

// @flow
type State = { +value: boolean };

type FooAction = { type: "FOO", foo: boolean };
type BarAction = { type: "BAR", bar: boolean };

type Action = FooAction | BarAction;

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case "FOO": return { ...state, value: action.foo };
    case "BAR": return { ...state, value: action.bar };
    default:
      (action: empty);
      return state;
  }
}

All 13 comments

If you read Dan's comment carefully, you will see that he recommends using string constants like const SOME_ACTION = 'SOME_ACTION' because they offer a few benefits. However, you get the exact same benefits and more using types. Types also force you to list all action constants in one place (the type definition), which produces to the same results:

  • Consistent naming.
  • Easy to find existing actions when adding new features.
  • Clear pull requests.
  • Typos are impossible.

Assuming you are already typing your actions using Flow, using type constants like const SOME_ACTION = 'SOME_ACTION' is completely redundant. More than that, it's a step backwards. String constants are just a convention, so nothing enforces them. Flow makes the types mandatory, so you will get nice errors if you make a mistake. This actually makes type-level strings superior to string constants.

I know it's weird to put string literals in code. It doesn't feel "clean", since all our instincts tell us this is a bad. However, those instincts come from a time before Flow. Now that we have a superior system, it's time to re-examine those old prejudices and adopt better ways of doing things.

I usually create a union type for all of my actions and each action is an exact object type. This allows flow to type check all of the strings which means it should be safe to use strings without having to define constants. Without using flow I would definitely recommend using constants since your linter can flag spelling mistakes. But with flow and exact object types it doesn't seem necessary.

@swansontec It doesn't convince me. I'm using constants for Action Types in Reducers, Actions and Sagas. Refusing constants is looking weird.

@kevinbarabash I got your idea, thanks.

@swansontec I rewrote all flow-types for every single redux-action with strings... I've got a really big mess.

@daryn-k You use terms like "looking weird" and "big mess". These are all emotional responses. You haven't given any objective reasons why it would actually be worse, like being harder to maintain.

I encourage you to move past your emotions, and think logically on this issue. Yes, it's weird. I agree. But sometimes weird is better.

Our team works on a bit app with a few hundred Redux action types. We didn't use Flow when we started, but only action constants. About a month ago, we finally got around to applying Flow to our action types. We discovered dozens of bugs, where where constants were defined in multiple places, or where the reducer was using one action but we were dispatching a different constant. This happened because constants are just a convention, and nothing enforces them. They are better than nothing, but not very effective. Now that we use Flow to type our actions, we have fewer lines of code, and Flow tells us right away when we mis-type anything. It took us a while to get used to the new system (since it does look weird), but now our team is much happier.

I made something like this yesterday:

// Define a constant and type cast it to enum (yes, this is ugly, but it seems to work))
export const MY_ACTION = ('namespace/MY_ACTION': 'namespace/MY_ACTION');

// Action object type
export type MyActionType = {
  type: typeof MY_ACTION,
  payload: { foo: string },
};

// export all action object types as union be used in e.g. reducer
export type AnyActionType = MyActionType | MyOtherActionType | ... // all action types

// Action creator
export const myAction = (foo: string): MyActionType => ({
  type: MY_ACTION,
  payload: { foo },
});

Based on quick experiments, this seems to type check reliably. But I would really like to have a method to type cast a string to enum to avoid the ugly duplication on the first row. At least I didn't find a way to do it.

Why not

export const MY_ACTION = "namespace/MY_ACTION"

@kevinbarabash Because when using typeof that type checks as any string. We want specific enum/string literal that represents the one exact "namespace/MY_ACTION" and not any other string.

As a sidenote, something like
export const MY_ACTION = ("namespace/MY_ACTION": Enum);
or
export const MY_ACTION: Literal<*> = "namespace/MY_ACTION";
for typecasting would be very handy to avoid duplication of code. I think I saw related discussion somewhere but I doubt anything like this is implemented yet.

@swansontec how do you store all your flow-types? I have hundreds actions, every action has its own specific payload. And every action and every payload have its own flow-type. The structure of whole project became very complicated. Can you demonstrate your file structure?

@daryn-k Sure, take a look at: https://github.com/EdgeApp/edge-core-js/blob/develop/src/modules/actions.js

If things get longer than this, you can split the types into different files, and then combine them together at the end:

import { FooAction } from './foo/action.js'
import { BarAction } from './bar/action.js'

export type RootAction = FooAction | BarAction

I think leaving the docs the way they are makes sense since Flow will error for types, etc. Also, not all of Dan's comments apply in the context of Flow or TS. For what it's worth, you can use constants, if you'd like:

````js
type State = { +value: boolean };

const FOO: 'FOO' = 'FOO';
const BAR: 'BAR' = 'BAR';

type FooAction = { type: typeof FOO, foo: boolean };
type BarAction = { type: typeof BAR, bar: boolean };

type Action = FooAction | BarAction;

function reducer(state: State, action: Action): State {
switch (action.type) {
case FOO: return { ...state, value: action.foo };
case BAR: return { ...state, value: action.bar };
default:
(action: empty);
return state;
}
}
````

[try flow]

Was this page helpful?
0 / 5 - 0 ratings