Overcoming application complexity with customized Flows

Engineering
September 6, 2023
Uri
Overcoming application complexity with customized Flows

Busy and wondering if you should spend your time reading this blog? Here’s why you should; in just a few minutes I’ll explain what XState is, what Flows are, and how you can use them to create smaller, shareable building blocks for your application’s logic. Flows are customized abstractions we created at 40Seas on top of XState, enabling you to manage the complexity of your application, test and debug efficiently, and make changes to parts of the application’s behavious without affecting the rest of the system.

Sounds Amazing! But… What is XState?

Good question 🙂

XState is a library for building state machines and statecharts. I highly recommend you check them out; they have great docs.

Simply put, a XState state machine is made up of different states, representing the different possible conditions or modes of operation of the system.

The concept of state machines often conjures images of deterministic finite automata, represented by boxes and arrows in diagrams. However, with XState, capabilities go beyond those of simple automation, offering the ability to save and update application context, effectively functioning like a Turing machine.

Let's discuss some of the key components of XState – Context, Actions, and Guards:

  • Context - represents the state of the system and serves as a shared and changeable data store that can be updated during the execution of the state machine.
  • Actions - functions which are triggered when a state is entered or exited. These functions can perform various effects such as modifying the Context or dispatching events.
  • Guards - functions that determine the eligibility of transitions to a new state. They assess conditions before allowing a transition to occur and can be linked to a specific transition or state.

That sounds amazing but.. What exactly are Flows and why do I need them?

Before we get into what Flows are, let’s talk about why we need them.

Consider the example of a company onboarding process, which can be different for very employee type:

We have shared steps (start with a “greeting” and finish with a “good luck” for all employees) and unique steps which we have only for the rnd employees (environment setup).

On one hand we’d like to embed a dedicated onboarding component on our company’s site, but on the other hand we’d like to minimize code duplication and maximize reusability. We achieve that by adding a layer of abstraction - splitting the process into small pieces, or Flows, and then combining the flows in order to build the whole process.

OK that really is amazing! So… what are Flows?

Every flow is composed of two main parts - the UI (react components) and the flow logic:

Let’s start with describing the logic:

  • Flow class - When implementing a new flow, we need to extend the Flow class:

Generally speaking, every class that extends this class is a small XState state machine, that must have the following properties:

  1. id - the flow’s id, has to be unique, and is used for flow concatenation.
  2. context  - the flow’s persisted data. This data can be shared with other flows, and binded to the UI etc.
  3. initalState - the id of the first state at the flow.
  4. states  - contain the different UI components / logical steps of the flow,
  5. guards - the transition conditions within the flow.
  6. shouldEnter and withShouldEnter - contains the logic that says whether or not we should enter this flow. It's ok if it is not super clear yet, we will explain that again in a minute, when we show how to connect flows together.

I think I got it but, can you share an example?

Of course, lets see an example of the greeting employee flow implementation:

As you can see, this flow contains only one state. At this state, we update the component which is presented on the screen using this code:

onEntry is a Xstate event that runs whenever we enter the state.
The loadPage is our code that replaces the currently presented component - 

This is actually the binding between the UI and the flow logic.

At the UI, we have the following code:

So basically whenever we update the component at the flow’s side, we render a different component.

Got it, I understand what the Flow class is. What’s next?

Great! The next important part of the flow logic is the Flow Provider.

Some flows contain business logic. For example, at the rnd’s onboarding we need to download the repo and install it, so we need to run many different commands, and we don’t want to put all this logic in the flow class, so we would put it at the flow’s provider.

The Flow Types contain the flow’s context type and the flow’s events type, which are exposed to the component using the Flow’s hook, so the component derives its state from the flow’s context, and sends actions to its current state. The component (and its backing flow) is not familiar with the whole context / events of the application and cannot use other flow’s events (as the machine’s state encapsulates it)


What about combining Flows?

Now that you’ve built a single Flows, the next step is to combine them. That’s the fun part!

In order to understand that, let's take a look at our onboarding process again and think about a possible way to model it as Flows:

We can break down the process into 3 steps, it looks something like this:

  1. greetEmployee - has one state with one page (e.g, one component)
  2. environmentSetupFlow - has two pages. Should be presented only for r&d onboarding. First state is conditional (show it only if the new employee does not have the terminal installed).
  3. goodLuckEmployee - has one state with one page

We have a dedicated function called wireFlows that combines the desired flows for each employee.

wireFlows?

If you're familiar with Residual Neural Networks, concatenating flows will sound familiar to you. The process involves going through a list of flows and, for each one, creating an "entryState" and an "exitState" that are added to the flow's "states."

The entryState will check if the flow should run and, if so, move to the first state of the flow, otherwise, it will move to the exit state, which in turn would move to the next entry flow state.

 In our example, we can create the help desk employee this way:

Similarly, we can define the rnd employee flow using the same building block:

We just need to make sure that within the setupEnvironmentFlow , the first inner flow needs to run only if  the terminal is not installed, we check that at the shouldEnter function, so it should be something like this -

Those new flows are extending the flow class as well, so we can embed them in other flows.

Let me make sure I got it right. You define a Flow, which is a unit that needs some arguments as input, it has some logic and UI.

You can concatenate multiple flows differently based on the server’s data?

Exactly! For the sake of simplicity, a flow can be analogized to a React component.

  1. The component purpose is to break down the UI into reusable independent pieces, each piece exposes an interface of props it needs.
  2. Similarly, a flow is a piece of the application general flow - can be reused,  decoupled from the rest, and exposes an interface that helps to connect it to the rest of the states.

Each flow can be developed independently, can be tested independently, and so on and so forth.

When we get the user data from the server, the client builds the relevant flow and instantiate an XState state machine.The final result is a bit more complicated then the one we described here and includes error handling and bank logic mechanism to navigate between the different flow steps, but that's a topic for another blog.

🥳 Good luck with your app!

Read more posts

Apply

40seas logo

Apply