小程序 使用redux
by Luca Matteis
卢卡·马蒂斯(Luca Matteis)
如何使用状态图对Redux应用程序的行为进行建模 (How to model the behavior of Redux apps using statecharts)
Our app, whether we like it or not, will be in a particular state at any given point in time. As we code User Interfaces (UI), we describe these states using data (a Redux store for instance), but we never give each state a formal name.
无论我们是否喜欢,我们的应用在任何给定时间点都将处于特定状态。 在编写用户界面(UI)时,我们使用数据(例如Redux存储)描述这些状态,但是我们从不为每个状态提供正式名称。
More importantly, there are events that should not be triggered when in a particular state.
更重要的是,某些事件处于特定状态时不应触发。
It turns out that this idea of describing states and the events that transition from one state into another is a well studied concept. Statecharts, for instance, provide a visual formalism for describing the behavior of reactive applications, such as user interfaces.
事实证明,描述状态和从一个状态转换为另一状态的事件的想法是一个经过充分研究的概念。 例如, 状态图为描述React性应用程序(例如用户界面)的行为提供了一种视觉形式。
In this article, I will discuss how the behavior of Redux apps can be decoupled from components, containers, or middlewares — places where we usually keep such logic — and can be contained and described entirely using a statechart. This allows for much easier refactoring and visualization of our application’s behavior.
在本文中,我将讨论Redux应用程序的行为如何与组件,容器或中间件(我们通常保留这种逻辑的地方)分离,并可以使用状态图完全包含和描述它们。 这样可以更轻松地重构和可视化我们应用程序的行为。
Redux和状态图 (Redux and statecharts)
Redux is quite simple. We have a UI component that triggers an event. We then dispatch
an action when this event occurs. A reducer uses this action to update the store. Finally, our component feeds directly from the updates to the store:
Redux非常简单。 我们有一个触发事件的UI组件。 然后,当此事件发生时,我们将dispatch
一个动作。 减速器使用此操作来更新商店。 最后,我们的组件直接将更新中的内容输入商店:
// our UI componentfunction Counter({ currentCount, onPlusClick }) { return <> <button onClick={onPlusClick}>plus</button> {currentCount} <>}
// let's connect the component to reduxconnect( state => ({ currentCount: state.currentCount }), dispatch => ({ onPlusClick: () => dispatch({ type: INCREMENT }) }))(Counter)
// handle the INCREMENT update using a reducerfunction currentCountReducer(state = 0, action) { switch(action.type) { case INCREMENT: return state + 1; default: return state; }}
This is pretty much all there is to Redux.
这几乎是Redux的全部。
To introduce statecharts, instead of mapping our event directly to the updating action, we map it to a generic action that doesn’t update any data (no reducer handles it):
为了引入状态图,我们将其映射到一个不更新任何数据(没有reducer处理的数据)的通用操作,而不是将事件直接映射到update操作。
// currently we are mapping our event to the update:// onPlusClick -> INCREMENT// instead, we dispatch a generic event which is not an update:// onPlusClick -> CLICKED_PLUS // this way we decouple our container from knowing // which update will happen.// the statechart will take care of triggering the correct update.
connect( state => ({ currentCount: state.currentCount }), dispatch => ({ onPlusClick: () => dispatch({ type: CLICKED_PLUS }) }))(Counter)
No reducer handles CLICKED_PLUS
so instead we let a statechart handle it:
没有化CLICKED_PLUS
器处理CLICKED_PLUS
所以我们让一个状态图来处理它:
const statechart = { initial: 'Init', states: { Init: { on: { CLICKED_PLUS: 'Increment' } }, Increment: { onEntry: INCREMENT, // <- update when we enter this state on: { CLICKED_PLUS: 'Increment' } } }}
The statechart will handle the events it receives similar to the way a reducer would, but only if it’s in a state that allows for such an event to happen. Events in this context are Redux actions that don’t update the store.
状态图将以与reducer相似的方式处理接收到的事件,但前提是处于允许此类事件发生的状态。 在这种情况下,事件是Redux动作,不会更新商店 。
In the above-mentioned example, we start off by being in the Init
state. When the CLICKED_PLUS
event occurs, we transition to the Increment
state which has an onEntry
field. This makes the statechart dispatch anINCREMENT
action — this time handled by a reducer, which updates the store.
在上述示例中,我们从处于Init
状态开始。 当发生CLICKED_PLUS
事件时,我们转换为具有onEntry
字段的Increment
状态。 这使状态图调度INCREMENT
操作-这次由reducer处理,从而更新商店。
You might be asking, why did we decouple the container from knowing about the update? We did it so that all of the behavior concerning when the update needs to occur is contained within the statechart JSON structure. Which means it can also be visualized:
您可能会问,为什么我们将容器与已知更新脱钩? 我们这样做是为了使所有有关何时需要进行更新的行为都包含在statechart JSON结构中。 这意味着它也可以可视化:
This can lead to improvements in the behavior of our app by simply changing the JSON description of the statechart. Let’s improve our design by grouping the twoCLICKED_PLUS
transitions into one, using the concept of hierarchical states:
只需更改状态图的JSON描述,就可以改善我们应用程序的行为。 让我们使用分层状态的概念将两个CLICKED_PLUS
转换分组为一个,来改进我们的设计:
To make this happen, we only had to change our statechart definition. Our UI components and reducers remain untouched.
为了做到这一点,我们只需要更改状态图定义。 我们的UI组件和Reducer保持不变。
{ initial: 'Init', states: { Init: { on: { CLICKED_PLUS: 'Init.Increment' }, states: { Increment: { onEntry: INCREMENT } } } }}
异步副作用 (Async side-effects)
Let’s imagine that when a <FetchDataButton
/> is clicked we want to start an HTTP request. Here’s how we would currently do it in Redux without statecharts:
想象一下,当单击<FetchDataButton
/>时,我们要启动HTTP请求。 这是我们目前在Redux中没有状态图的情况下的操作方式:
connect( null, dispatch => ({ onFetchDataClick: () => dispatch({ type: FETCH_DATA_CLICKED }) }))(FetchDataButton)
Then we would probably have an epic to handle such action. Below we are using redux-observable, but redux-saga or redux-thunk can be used as well:
然后,我们可能会有一部史诗片来处理这种动作。 下面我们使用redux-observable ,但是也可以使用redux-saga或redux-thunk:
function handleFetchDataClicked(action$, store) { return action$.ofType('FETCH_DATA_CLICKED') .mergeMap(action => ajax('http://foo.bar') .mapTo({ type: 'FETCH_DATA_SUCCESS' }) .takeUntil(action$.ofType('FETCH_DATA_CANCEL')) )}
Even though we decoupled the container from the side-effect (the container is simply telling the epic “hey, the fetch data button was clicked”), we still have the problem that the HTTP request is triggered no matter the state we’re in.
即使我们将容器从副作用中解耦了(容器只是告诉史诗“嘿,点击了获取数据按钮”),我们仍然有一个问题,无论我们处于什么状态,都将触发HTTP请求。
What if we’re in a state where FETCH_DATA_CLICKED
should not trigger an HTTP request?
如果我们处于FETCH_DATA_CLICKED
不应触发HTTP请求的状态,该怎么办?
This case can easily be handled by statecharts. When FETCH_DATA_CLICKED
happens, we transition to a FetchingData
state. Only when entering this state (onEntry
) does the FETCH_DATA_REQUEST
action get dispatched:
这种情况可以通过状态图轻松处理。 当发生FETCH_DATA_CLICKED
时,我们转换为FetchingData
状态。 仅当进入此状态( onEntry
)时, FETCH_DATA_REQUEST
调度FETCH_DATA_REQUEST
操作:
{ initial: 'Init', states: { Init: { on: { FETCH_DATA_CLICKED: 'FetchingData', }, initial: 'NoData', states: { ShowData: {}, Error: {}, NoData: {} } }, FetchingData: { on: { FETCH_DATA_SUCCESS: 'Init.ShowData', FETCH_DATA_FAILURE: 'Init.Error', CLICKED_CANCEL: 'Init.NoData', }, onEntry: 'FETCH_DATA_REQUEST', onExit: 'FETCH_DATA_CANCEL', }, }}
Then we change our epic to react based on the newly added FETCH_DATA_REQUEST
action instead:
然后,我们将史诗更改为根据新添加的FETCH_DATA_REQUEST
操作做出React:
function handleFetchDataRequest(action$, store) { // handling FETCH_DATA_REQUEST rather than FETCH_DATA_CLICKED return action$.ofType('FETCH_DATA_REQUEST') .mergeMap(action => ajax('http://foo.bar') .mapTo({ type: 'FETCH_DATA_SUCCESS' }) .takeUntil(action$.ofType('FETCH_DATA_CANCEL')) )}
This way the request will be triggered only when we’re in the FetchingData
state.
这样,仅当我们处于FetchingData
状态时,才会触发请求。
Again, by doing so, we pushed all of the behavior inside the JSON statechart, making refactoring easy and allowing us to visualize something that would’ve otherwise remained hidden in code:
再次,通过这样做,我们将所有行为推送到JSON状态图中,使重构变得容易,并允许我们可视化本来可以隐藏在代码中的内容:
An interesting property of this particular design is that when we exit the FetchingData
state, the FETCH_DATA_CANCEL
action is dispatched. We can dispatch actions not only when entering states, but also when exiting them. As defined in our epic, this will cause the HTTP request to abort.
此特定设计的一个有趣特性是,当我们退出FetchingData
状态时,将调度FETCH_DATA_CANCEL
操作。 我们不仅可以在进入状态时,还可以在退出状态时调度动作。 如我们的史诗中所定义,这将导致HTTP请求中止。
It’s important to note that I added this particular HTTP-abort behavior only after looking at the resulting statechart visualization. By simply glancing at the diagram, it was apparent that the HTTP request should’ve been cleaned up when exiting FetchingData
. This might have not been so apparent without such visual representation.
重要的是要注意,仅在查看结果状态图可视化之后,我才添加了此特定的HTTP中止行为。 通过简单地浏览一下该图,很明显在退出FetchingData
时应该已经清除了HTTP请求。 没有这种视觉表现,这可能不是那么明显。
By now, we can gather the intuition that statecharts control our store updates. We learn which side-effects need to happen and when they need to happen, based on the current state we’re in.
到目前为止,我们可以了解状态图控制商店更新的直觉。 我们根据当前所处的状态来了解需要发生哪些副作用以及何时需要发生这些副作用。
The main insight here is that our reducers and epics will always react based on the output actions of our statechart, rather than our UI.
这里的主要见解是,我们的化简和史诗将始终根据状态图的输出动作做出React,而不是基于UI。
In fact a statechart can be implemented as a stateful event-emitter: you tell it what happened (trigger an event), and, by remembering the last state you were in, it tells you what to do (actions).
实际上,状态图可以实现为有状态的事件发射器:您可以告诉它发生了什么(触发一个事件),并且通过记住您所处的最后一个状态,可以告诉您要做什么(动作)。
状态图有助于解决的问题 (Problems statecharts help solve)
As UI developers, our job is to bring static images to life. This process has several problems:
作为UI开发人员,我们的工作是使静态图像栩栩如生。 这个过程有几个问题:
When we convert static images into code we lose the high-level understanding of our app — as our app grows, understanding which section of code is responsible for each image becomes increasingly difficult.
当我们将静态图片转换为代码时,我们会失去对应用程序的高级理解 - 随着我们应用程序的增长,了解哪个代码段负责每个图像变得越来越困难。
Not all questions can be answered using a set of images — What happens when the user clicks the button repeatedly? What if the user wants to cancel the request while it’s in-flight?
并非所有问题都可以使用一组图像来回答 -当用户反复单击按钮时会发生什么? 如果用户想在进行中取消请求怎么办?
Events are scattered across our code and have unpredictable effects — When the user clicks a button, what happens exactly? We need a better abstraction that helps us understand the repercussions of firing events.
事件分散在我们的代码中,并具有不可预测的效果 -当用户单击按钮时,究竟发生了什么? 我们需要更好的抽象,以帮助我们理解触发事件的影响。
Lots of
isFetching
,isShowing
,isDisabled
variables — We need to keep track of everything that changes in our UI.很多
isFetching
,isShowing
,isDisabled
变量 -我们需要跟踪UI中所有更改的内容。
Statecharts help solve these problems by providing a strict visual formalism of the behavior of our app. Drawing a statechart allows us to have a high-level understanding of our app which lets us answer questions using visual clues.
状态图通过提供严格的视觉形式主义来帮助解决这些问题 我们应用程序的行为 绘制状态图可以使我们对应用程序有一个高层次的了解,这使我们可以使用视觉线索回答问题。
All the states of an app are explored during this process and events are explicitly labelled, allowing us to predict what is going to happen after any given event.
在此过程中,将探究应用程序的所有状态,并显式标记事件,从而使我们能够预测任何给定事件后将发生的情况。
Furthermore, a statechart can be constructed directly from designers’ mockups, allowing non-engineers to also understand what is happening without having to dig into actual code.
此外,状态图可以直接从设计人员的模型中构建,从而使非工程师也可以了解正在发生的事情,而不必深入研究实际代码。
学到更多 (Learn more)
As a concrete example of this, I’ve built redux-statecharts, a Redux middleware that can be used as shown in the earlier examples. It uses the xstate library — a pure function for transitioning a statechart.
作为一个具体的示例,我构建了redux-statecharts ,这是一个Redux中间件,可以如前面的示例中所示使用。 它使用xstate库-用于转换状态图的纯函数。
If you’d like to learn more about statecharts, here is a great resource: https://statecharts.github.io/
如果您想了解有关状态图的更多信息,请访问以下网址 : https : //statecharts.github.io/
Also check out my presentation on the subject: Are statecharts the next big UI paradigm?
还请查看我关于以下主题的演示文稿: 状态图是下一个主要的UI范式吗?
小程序 使用redux