by Fabian Terh
由Fabian Terh
This article is divided into 4 sections:
本文分为4个部分:
While there are a bunch of guides on the first section which are immensely helpful, I found only a paucity of articles on 3 and 4. After a long session of Google searches, diving into source code, and trial and error, I decided to put together what I’ve learned and write this tutorial as a one-stop guide to type checking your React + Redux + React-Redux application with Flow.
尽管在第一部分中有很多指南对您有很大帮助,但是我发现关于3和4的文章很少。经过一番Google搜索,深入研究源代码以及反复试验之后,我决定放下结合我所学到的知识,并将本教程作为一站式指南来编写,以使用Flow对您的React + Redux + React-Redux应用程序进行类型检查。
Redux actions are essentially vanilla Javascript objects with a mandatory type
property:
Redux操作本质上是具有强制type
属性的原始Javascript对象:
// This is an action{ type: 'INCREASE_COUNTER', increment: 1}
Following best practices, you may want to define and use action type constants instead. If so, the above snippet would probably look something like this:
按照最佳实践,您可能想要定义和使用操作类型常量 。 如果是这样,上面的代码段可能看起来像这样:
const INCREASE_COUNTER = 'INCREASE_COUNTER';
// This is an action{ type: INCREASE_COUNTER, increment: 1}
Type checking is easy (we’re dealing with regular JavaScript here):
类型检查很容易(我们在这里处理常规JavaScript):
type $action = { type: 'INCREASE_COUNTER', increment: number};
Note that you cannot substitute the literal string type with the constant INCREASE_COUNTER
. This is a limitation of Flow itself.
注意,不能用常量INCREASE_COUNTER
代替文字字符串类型 。 这是Flow本身的局限性 。
Since action creators are just functions that return actions, we’re still dealing with regular Javascript. This is how a type checked action creator can look like:
由于动作创建者只是返回动作的函数,因此我们仍在处理常规Javascript。 这是类型检查动作创建者的样子:
function increaseCounter(by: number): $action { return { type: INCREASE_COUNTER, // it's okay to use the constant here increment: by };}
Reducers are functions that handle actions. They receive a state and an action, and return the new state. At this juncture, it’s important to think about how your state will look like (state shape). In this very simple example, the state shape comprises of only a single key counter
which takes on a number
type value:
减速器是处理动作的功能。 他们收到一个状态和一个动作,并返回新状态。 在此关头,考虑一下您的状态(状态形状)是很重要的。 在这个非常简单的示例中,状态形状仅包含一个带有number
类型值的键counter
:
// State shape{ counter: <number>}
And so your reducer could look like this:
因此,您的减速器可能如下所示:
const initialState = { counter: 0 };
function counter(state = initialState, action) { switch (action.type) { case INCREASE_COUNTER: return Object.assign({}, state, { counter: action.increment + state.counter }); default: return state; }};
Note: In this particular example, Object.assign({}, state, { ... })
is redundant because the store consists only of 1 key/value pair. I could just as easily return the last argument to the function. However, I included the full implementation for correctness.
注意:在此特定示例中, Object.assign({}, state, { ... })
是多余的,因为存储仅由1个键/值对组成。 我可以轻松地将最后一个参数返回给函数。 但是,为了正确起见,我包含了完整的实现。
Typing the state and reducer is simple enough. Here is the typed version of the above snippet:
输入状态和reducer很简单。 这是类型 以上代码段的版本:
type $state = { +counter: number};
const initialState: $state = { counter: 0 };
function counter( state: $state = initialState, action: $action): $state { switch (action.type) { case INCREASE_COUNTER: return Object.assign({}, state, { counter: action.increment + state.counter }); default: return state; }};
Flow library definitions (or libdefs) provide type definitions for third-party modules. In this case, we are using React, Redux, and React-Redux. Instead of typing these modules and their functions manually, you can install their type definitions using flow-typed
:
流库定义 (或libdefs)为第三方模块提供类型定义。 在这种情况下,我们使用React,Redux和React-Redux。 您可以使用flow-typed
安装它们的类型定义,而不必手动键入这些模块及其功能:
npm install -g flow-typed
// Automatically download and install all relevant libdefsflow-typed install
// Orflow-typed install <package>@<version> // e.g. redux@4.0.0
Library definitions are installed to the flow-typed
folder, which lets Flow work out of the box without any further configuration (details).
库定义将安装到flow-typed
文件夹中,这使Flow可以直接使用,而无需任何进一步的配置( 详细信息 )。
Previously, we have already typed the state like this:
以前,我们已经输入了如下状态:
type $state = { +counter: number};
While this works for a simple example like the one above, it breaks down once your state becomes significantly larger. You would have to manually edit type $state
every time you introduce a new reducer or modify an existing one. You also wouldn’t want to keep all your reducers in the same file. What you want to do instead is to refactor your reducers into separate modules, and use Redux’s combineReducers
function.
尽管这对于上面的示例很简单,但是一旦您的状态显着变大,它就会崩溃。 每次引入新的reducer或修改现有的reducer时,都必须手动编辑type $state
。 您也不想将所有的reducer放在同一文件中。 相反,您要做的是将减速器重构为单独的模块,并使用Redux的combineReducers
函数。
Since the focus of this article is on type checking a React/Redux/React-Redux application, and not on building one, I’m going to assume you are familiar with the combineReducers
function. If not, head over to Redux’s tutorial to learn all about it.
由于本文的重点是对React / Redux / React-Redux应用程序进行类型检查 ,而不是构建应用程序,因此,我假定您熟悉combineReducers
函数。 如果没有,请转到Redux的教程以了解所有内容。
Suppose we introduce a new action/reducer pair in a separate module:
假设我们在单独的模块中引入了一个新的动作/减速器对:
// playSong.js
export const PLAY_SONG = 'PLAY_SONG';
// Typing the actionexport type $playSongAction = { type: 'PLAY_SONG', song: ?string};
// Typing the action creatorexport function playSong(song: ?string): $playSongAction { return { type: PLAY_SONG, song: song };};
// Typing arg1 and the return value of the reducer [*1]export type $song = ?string;
// Typing the state [*1]export type $songState = { +song: $song};
// [*1][*2]const initialValue: $song = null;
// Typing the reducer [*1][*3]function song( state: $song = initialValue, action: $playSongAction): $song { switch (action.type) { case PLAY_SONG: return action.song; default: return state; }};
[*1]: If we’re using the combineReducers
function, it’s important to note that your reducer should no longer be returning the state, but rather the value to the key in the state. In this regard, I think Redux’s tutorial is a bit lacking in clarity, as it does not state this explicitly, although it is clear from the example code snippets.
[* 1]:如果我们使用combineReducers
函数,则需要注意的是, reduce不再应该返回状态,而应返回state中键的值 。 在这方面,我认为Redux的教程有点不够清晰,因为虽然从示例代码片段中可以清楚地看到,但它没有明确说明。
[*2]: Reducers are not allowed to return undefined
, so we have to settle for null
.
[* 2]:减速器不允许返回undefined
,因此我们必须满足null
。
[*3]: Since the reducer is no longer receiving and returning a state in the form of { song: string }
, but rather the value to the song
key in the state object, we need to change the types of its first argument and return value from $songState
to $song
.
[* 3]:由于reducer不再以{ song: string }
的形式接收和返回状态,而是状态对象中song
键的值 ,因此我们需要更改其第一个参数的类型,并从$songState
到$song
返回值。
We modify and refactor increaseCounter
as well:
我们修改和重构increaseCounter
还有:
// increaseCounter.js
export const INCREASE_COUNTER = 'INCREASE_COUNTER';
export type $increaseCounterAction = { type: 'INCREASE_COUNTER', increment: number};
export function increaseCounter(by: number): $action { return { type: INCREASE_COUNTER, increment: by };};
export type $counter = number;
export type $counterState = { +counter: $counter};
const initialValue: $counter = 0;
function counter( state: $counter = initialValue, action: $increaseCounterAction): $counter { switch (action.type) { case INCREASE_COUNTER: return action.increment + state; default: return state; }};
Now we have 2 action/reducer pairs.
现在我们有2个动作/减速器对。
We can create a new State
type to store the type of our application state:
我们可以创建一个新的State
类型来存储我们的应用程序状态的类型:
export type State = $songState & $counterState;
This is a Flow intersection type, and is equivalent to:
这是流交叉点类型 ,等效于:
export type State = { song: $song, counter: $counter};
If you don’t want to create $songState
and $counterState
only for use in intersection-typing the application state State
, that’s perfectly fine too — go with the second implementation.
如果您不想创建$songState
和$counterState
仅用于交叉键入应用程序状态State
,那也很好-与第二个实现一起进行。
I found that Flow was reporting errors in my containers (in the context of the container/component paradigm).
我发现Flow正在报告我的容器中的错误(在容器/组件范例的上下文中)。
Could not decide which case to select. Since case 3 [1] may work but if it doesn't case 6 [2] looks promising too. To fix add a type annotation to dispatch [3].
This was with regard to my mapDispatchToProps
function. Cases 3 and 6 are as follows:
这与我的mapDispatchToProps
函数有关。 情况3和6如下:
// flow-typed/npm/react-redux_v5.x.x.js
// Case 3declare export function connect< Com: ComponentType<*>, A, S: Object, DP: Object, SP: Object, RSP: Object, RDP: Object, CP: $Diff<$Diff<ElementConfig<Com>;, RSP>, RDP> >( mapStateToProps: MapStateToProps<S, SP, RSP>, mapDispatchToProps: MapDispatchToProps<A, DP, RDP>): (component: Com) => ComponentType<CP & SP & DP>;
// Case 6declare export function connect< Com: ComponentType<*>, S: Object, SP: Object, RSP: Object, MDP: Object, CP: $Diff<ElementConfig<Com>, RSP> >( mapStateToProps: MapStateToProps<S, SP, RSP>, mapDispatchToPRops: MDP): (component: Com) => ComponentType<$Diff<CP, MDP> & SP>;
I don’t know why this error occurs. But as the error hints, typing dispatch
fixes it. And if we’re typing dispatch
, we might as well type store
too.
我不知道为什么会发生此错误。 但是如错误提示所示,键入dispatch
解决该问题。 而且,如果我们输入dispatch
,我们也可以输入store
。
I couldn’t find much documentation on this aspect of typing a Redux/React-Redux application. I learned by diving into the libdefs and looking at the source code for other projects (albeit a demonstration project). If you have any insights, please let me know so I can update this post (with proper attribution, of course).
在键入Redux / React-Redux应用程序的这一方面,我找不到很多文档。 我通过深入libdefs并查看其他项目 (尽管是演示项目)的源代码来学习 。 如果您有任何见解,请告诉我,以便我可以更新此帖子(当然,请注明出处)。
In the meantime, I’ve found that this works:
同时,我发现这可行:
import type { Store as ReduxStore, Dispatch as ReduxDispatch} from 'redux';
// import any other variables and types you may need,// depending on how you organized your file structure.
// Reproduced from earlier onexport type State = { song: $song, counter: $counter};
export type Action = | $playSongAction | $increaseCounterAction
export type Store = ReduxStore<State, Action>;
export type Dispatch = ReduxDispatch<Action>;
Heading into your container modules, you can then proceed to type mapDispatchToProps
as follows: const mapDispatchToProps = (dispatch: Dispatch) => { ...
};
进入容器模块,然后可以按如下所示继续输入mapDispatchToProps
: const mapDispatchToProps = (dispatch: Dispatch) => { ...
};
This has been a pretty long post, and I hope you found it useful. I wrote it partly because of a dearth of resources regarding the later sections of this article (and partly to organize my thoughts and consolidate what I’ve learned).
这是一篇很长的文章,希望您觉得它有用。 我写这篇文章的部分原因是由于本文后面各部分的资源匮乏(部分原因是为了整理我的思想并巩固我所学的知识)。
I can’t guarantee that the guidance in this article follows best practices, or is conceptually sound, or even 100% correct. If you spot any errors or issues, please let me know!
我不能保证本文中的指导遵循最佳实践,或者从概念上讲是正确的,甚至100%正确。 如果您发现任何错误或问题,请告诉我!