States and React: step-by-step user interaction with state machines

Christoph Dähne09.11.2017

We at Sandstorm love building rich and powerful applications based on React, Redux and Saga. Yes, we would recommend it and use it in our own products. Advanced, feature–rich applications guiding the user without restricting his control pose a challenge to each application developer. This blog post explains one pattern to implement step–by–step interaction, i.e. wizards, in an easy, robust and scalable way.

In order to understand the following example you need to know the basics about JavaScript and Saga and know the definition of a finite state–machine.

Let's get our hands dirty…

Every now and then we want to inform the user about what is happening without interrupting his current activity. We want to show the user a short message and let him decide to either

  • ignore it and let it fade away
  • close it manually
  • react to it and get involved

We talk about a Snackbar, of course. To keep things simple here, all messages are the same: no different colors, different time-outs or any fancy message–related features.

A Snackbar displays a temporary, closable notification related to the operation performed.

Snackbar Example

In order to show a notification in a snackbar we perform three steps: set everything up and open it, wait for user–interaction or a time–out, clean–up and reset. Our example state–machine consists of those three states we call: opening, visible and closing.

Snackbar Example

Opening State: put things into place

In order to show a notification we have to set some values in the Redux Store. Since the user cannot interact yet with the initialization, he cannot interfere with the setup in mid–flight. In this small example we just set the current notification and proceed to the next state.

 

function * state_opening(notification) { yield put(actions.UI.Snackbar.setCurrentNotification(notification)); yield call(snackbarLifeCycle, Snackbar_LifeCycleStates.Visible); }

Visible State: ready for interaction

Now we are ready for the user. The system initialization is complete and only now the user interaction is possible: no race–conditions during the setup. When done we move to the next state.

 

function * state_visible() { const {triggerAction} = yield race({ timeout: delay(5000 /* 5 s */), // ignore it and let it fade away dismiss: take(actionTypes.UI.Snackbar.DISMISS), // close it manually triggerAction: take(actionTypes.UI.Snackbar.TRIGGER_ACTION) // react to it and get involved }); if (triggerAction) { yield put($get('payload.action', triggerAction)); } yield call(snackbarLifeCycle, Snackbar_LifeCycleStates.Closing); }

Closing State: time to let go

Whatever the reason, now we remove the notification and clean up the application state. Since this example is so small this is a one–liner again. Note that we do not have to leave the state machine – we just terminate the Closing state without transitioning to a next state. The JavaScript garbage collection takes care of everything for us.

 

function * state_closing() { yield put(actions.UI.Snackbar.setCurrentNotification(null)); }

The life–cycle: putting it all together

Here is the glue–code to connect all the functions implementing the single states.

 

function * snackbarLifeCycle(state, notification) { switch (state) { case Snackbar_LifeCycleStates.Opening: yield call(state_opening, notification); break; case Snackbar_LifeCycleStates.Visible: yield call(state_visible); break; case Snackbar_LifeCycleStates.Closing: yield call(state_closing); break; default: throw new Error('unknown life cycle state:', state); } } export default function * SnackbarSaga() { yield takeLatest(actionTypes.UI.Snackbar.SHOW, function * (action) { const notification = $get('payload.notification', action); if (notification) { yield call(snackbarLifeCycle, Snackbar_LifeCycleStates.Opening, notification); } }); }

Effects & Benefits of a Snackbar

Without any doubt, it is possible to implement this small Snackbar Example in another way. This approach however scales to much more complex interaction models. Once you got the hang of it you just use it. We took the Snackbar Example from production code by the way.

Less code & smaller Redux store

The state–machine code tends to be very small and everything resides in one source file with one export. Since different states may pass arguments to each other the Redux store contains only visible values used for rendering: no intermediate result, no scheduling and not even the current state.

Thus refactoring and extension is rather easy.

Automatic clean–up

In our Snackbar example it might happen that a notification spawns while another one is visible. In this case we must reset the state–machine and clean–up old state to replace the old notification with the new one — but we don't have to write any line of code. The Redux store contains only visible information and any other state is garbage collected automatically: takeLatest terminates the call of snackbarLifeCycle and the runtime–stack is removed when the new notification arises. Pending remote calls are canceled.

Asynchronous Steps

The life–cycle implementation makes it impossible for state–executions to overlap: opening happens before visible. This reduces the risk of race–condition related bugs. Also waiting for remote calls or user interaction is very easy by using yield. It reads like a step–by–step execution of synchronous calls. State machine resets even cancel pending remote calls.

Common knowledge

Most developers know state–machines to some degree and there are a lot of tools available to work with them. Agreeing on this implementation style makes working together in a team much more easy and fun.

Game Level Example

In the Snackbar Example the benefits of this state–machine design and implementation might be not clear due to the simplicity of this example. It is great for explanation and average for motivation. Now that you read this post I want to show you another state–machine to outline the benefits. Just think about how you would implement it and how you would have before reading this post.

React Statemachine

A great "Thank you!"

Writing this article with the new Neos React UI which uses this pattern I want to thank the Neos Guys for the great work. I learned about this pattern from those smart people. Now I love it and want to share it myself.

Dein Besuch auf unserer Website produziert laut der Messung auf websitecarbon.com nur 0,28 g CO₂.