Finite State Machines with @wordpress/data and TypeScript

Published in WordPress.

Watch our video

There is a better version of your web

Comparison of two variants of the same page using A/B testing

Share this post

Suppose we want to design a login screen in our application. When the user sees it for the first time, there’s an empty form. The user fills in the fields and, once they’re all set, they press the login button to validate the credentials and, well, log in. If the validation succeeds, they move to the next screen. But if it doesn’t, they’re presented an error message and are requested to try again.

If you were to implement the state of such a screen and you read my posts on how to define the state of an application with TypeScript and @wordpress/data, I bet you’d do something like this:

type State = {
  readonly username: string;
  readonly password: string;
  readonly isLoggingIn: boolean;
  readonly errorMessage: string;
};

which has all the necessary fields to manage the state but… is this the best we can do?

Finite State Machines

A finite state machine (FSM) is, broadly speaking, a mathematical model that allows us to define a finite set of states and the possible transitions between them. (more information on Wikipedia). I like this abstraction because it perfectly fits our needs when it comes to modeling the state of our applications. For example, the state diagram for our login screen might look something like this:

State diagram of a login form
State diagram of a login form.

which, as you can see, captures in a clear and concise way the behavior we want to implement.

Defining States with @wordpress/data

In our introductory series to React, we saw how to use the @wordpress/data package to create and manage our app’s state. We did so by defining the main components of our data store:

  • A set of selectors to query the state
  • A set of actions to trigger an update request
  • A reducer function to, given the current state and an update action, update the state

In essence, stores in @wordpress/data already behave like finite state machines, since the reducer function “transitions” from one state to another using an action:

( state: State, action: Action ) => State

How to Define a Finite State Machine with TypeScript in @wordpress/data

It looks like @wordpress/data stores are pretty close to the FSM model we introduced a few lines above: we’re just missing the specific states we can be in and the transitions between them… So let’s take a closer look at how we can fix these issues, one step at a time.

Explicit States

We started this post by showing a possible implementation of our app’s state. Our first proposal was a bunch of attributes that got the job done, but it didn’t look like a finite state machine at all. So let’s start by making sure our states are explicitly define in our model:

type State =
  | Form
  | LoggingIn
  | Success
  | Error;

type Form = {
  readonly status: 'form';
  readonly username: string;
  readonly password: string;
};

type LoggingIn = {
  readonly status: 'logging-in';
  readonly username: string;
  readonly password: string;

};

type Success = {
  readonly status: 'success';
  // ...
};

type Error = {
  readonly status: 'error';
  readonly message: string;
};

Pretty obvious, huh? There’s one type per state and the app’s overall state is defined as a discriminated union of types. It is so called because (a) the state is “the union of different types” (that is, it is either Form, or it is LoggingIn, or it is whatever) and (b) we have an attribute (in in this case, status) that allows us to discriminate what specific state we have at each moment.

I think this solution is way better than the one we had at the beginning. In our original solution we were able to define “invalid states,” because one could be logging in (just set isLoggingIn to true) and, at the same time, be in an error state (just set errorMessage to a value other than the empty string). But this clearly doesn’t make sense! Which one is it? Are we logging in or are we supposed to show an error?

The new solution, on the other hand, is much more precise when it comes to representing the state: it makes “invalid” states “impossible.” If we are in LoggingIn, there’s no way to set up an error message (there’s no attribute for it!). If we’re showing an Error, you can’t be logging in. Way better, don’t you think?

Actions

In @wordpress/data, actions update the store in our state. Since we’re modeling our state as a FSM, our actions will be the arrows from our original diagram. All you have to do is model them as a discriminated union of types (by convention, the discriminator is usually named type) and you’re good to go:

type SetCredentials = {
  readonly type: 'SET_CREDENTIALS';
  readonly username: string;
  readonly password: string;
};

type Login = {
  readonly type: 'LOGIN';
};

type ShowApp = {
  readonly type: 'SHOW_APP';
  // ...
};

type ShowError = {
  readonly type: 'SHOW_ERROR';
  readonly message: string;
};

type BackToLoginForm = {
  readonly type: 'BACK_TO_LOGIN_FORM';
};

Our actions are supposed to represent the arrows we had in our original diagram, right? Well, I don’t know about you, but I think these action types don’t look like arrows at all! Arrows have a direction (they go from one state to another); actions don’t.

We need a way to make it clear in our code that certain actions are only useful to move from one state to another. And so far the best solution I’ve found is to use the reducer itself:

function reducer( state: State, action: Action ): State {
  switch ( state.status ) {
    case 'form':
      switch ( action.type ) {
        case 'SET_CREDENTIALS':
           return {
             status: 'ready',
             username: action.username,
             password: action.password,
           };
        case 'LOGIN':
           return {
              ...state,
              status: 'logging-in'
           };
      }
      case ...:
        ...
  }
  return state;
}

If our reducer starts by filtering the status of our state, we know the “source” of our arrow. Then, we look at the current action and, if it’s an “outward arrow,” we generate the new “target” state.

You can clearly see how this works in the snippet above. If the current status is form, the SET_CREDENTIALS action takes us to the same state we were in (updating the credentials, duh) and the LOGIN action changes the state to logging-in. Any other action in this state is ignored and, therefore, the state is unchanged.

That’s all good and perfect but… I don’t know, I don’t like the code that much, do you? I want to make sure my code is clear, concise, self-explanatory, and type-safe. Can we do that?

Strongly Typed Reducer

To fix the mess we just got into, we just need to refactor our reducer so that each possible state in our FSM has its own reducer. If we do this correctly, they’ll be strongly typed and it’ll be clear what’s allowed and what isn’t.

Let’s start by creating a type for each state in our application. Each type will group the actions that can take us from the given state to somewhere else. For example, our Form state has two outward arrows (SetCredentials and Login), so let’s create the FormAction type with the union of the two actions. Repeat the process for each state and you’ll get this:

type FormAction = SetCredentials | Login;
type LoggingInAction = ShowError | ShowApp;
type SuccessAction = ...;
type ErrorAction = BackToLoginForm;

type Action =
  | FormAction
  | LoggingInAction
  | SuccessAction
  | ErrorAction;

Next, define all the reducers we need. Again, one for each state:

function reduceForm( state: Form, action: FormAction ): Form | LoggingIn {
  switch ( action.type ) {
      switch ( action.type ) {
        case 'SET_CREDENTIALS':
           return {
             status: 'ready',
             username: action.username,
             password: action.password,
           };
        case 'LOGIN':
           return {
              ...state,
              status: 'logging-in'
           };
      }
}

reduceLoggingIn: ( state: LoggingIn, action: LoggingInAction ) => Success | Error
reduceError: ( state: Error, action: ErrorAction ) => Form
reduceSuccess: ( state: Success, action: SuccessAction ) => ...

Thanks to TypeScript and the types we just defined, we can now be extremely precise about the source state and the set of actions each reducer expects, as well as the target state(s) we can get as a result.

Finally, we simply need to tie it all up with a unique reducer function:

function reducer( state: State, action: Action ): State {
  switch ( state.status ) {
    case 'form':
      return reduceForm( state, action as FormActions ) ?? state;
    case 'logging-in':
      return reduceLoggingIn( state, action as LoggingInActions ) ?? state;
    case 'error':
      return reduceError( state, action as ErrorActions ) ?? state;
    case 'success':
      return reduceSuccess( state, action as SuccessActions ) ?? state;
  }
}

The resulting code is, in my opinion, clearer and easier to maintain. And, on top of that, TypeScript is now able to verify what we’re doing and catch any missing scenarios. Awesome!

Conclusion

In this post we’ve seen what a finite state machine is and how we can implement one in a @wordpress/data store with TypeScript. Our final solution is quite simple and, yet, it offers a lot of advantages: it captures the state and intent of our app better, and it leverages the power of TypeScript.

I hope you liked this post! If you did, please share it with your friends and colleagues. And please let me know how you tackle these problems in the comment section below.

Featured image by Patrick Hendry on Unsplash.

Leave a Reply

Your email address will not be published. Required fields are marked: •

I have read and agree to the Nelio Software Privacy Policy

Your personal data will be located on SiteGround and will be treated by Nelio Software with the sole purpose of publishing this comment here. The legitimation is carried out through your express consent. Contact us to access, rectify, limit, or delete your data.