In part 1, you learned about the concepts behind state machines, how to create a basic working machine using XState, and integrate that into a React component.
In the second part, we will make our traffic light run automatically and turn it ON and OFF.
This article will assume you already read part 1. So if you didn’t, please check it out.
Last time, our state machine modeled a traffic light that changed at the press of a button. Now it’s time to add more features.
Making the Traffic Light Automatic
For a more realistic implementation, the button needs to be replaced by a timer. There are several ways to implement it. For this example, the choice will be a Delayed Transition — a transition that occurs automatically after a delay.
export const trafficLightMachine = createMachine< undefined, TrafficLightEvent, TraffiLightState >({ id: "trafficLight", initial: "red", states: { green: { on: { NEXT: "yellow" }, after: { 3500: "yellow", }, }, yellow: { on: { NEXT: "red" }, after: { 1500: "red", }, }, red: { on: { NEXT: "green" }, after: { 5000: "green", }, }, }, });
These delayed transitions are defined in the after property, which maps the delay in milliseconds to the respective state it will transition to.
So, 3.5 seconds after entering the green state, it will automatically transition to the yellow state. Which, in turn, will transition to the red state after 1.5 seconds, and red will transition over to green after 5 seconds, completing the familiar traffic light loop.
After making this change, the lights will start changing automatically, just like a real traffic light.
You can still use the NEXT button, and it will change lights immediately. Let’s run our automatic traffic light machine through the visualizer again.
Our automatic traffic light state machine
If you do it yourself, you’ll notice that the automatic transitions also happen on the visualizer! This is incredibly helpful to visualize complex business logic and a great way for designers and product managers to visualize how the application works.
Hierarchical States
A new requirement just came in. We need to be able to turn the traffic light on and off. So it’s time to extend our machine definition.
The machine needs to be turned on and off, which required adding two more event types to our model.
type TrafficLightEvent = | { type: "NEXT" } | { type: "TURN_OFF" } | { type: "TURN_ON" };
Now to our states, when the machine is ON, the lights should cycle automatically, and when we are on the OFF state, the lights need to be all off. This is best represented by hierarchical states — the OFF state would be a simple state, and the ON state should also be able to house our existing states. Let’s see how that looks.
type TraffiLightState = | { value: { ON: "green" }; context: undefined } | { value: { ON: "yellow" }; context: undefined } | { value: { ON: "red" }; context: undefined } | { value: "OFF"; context: undefined }; export const trafficLightMachine = createMachine< undefined, TrafficLightEvent, TraffiLightState >({ id: "trafficLight", initial: "OFF", states: { ON: { on: { TURN_OFF: "OFF" }, initial: "red", states: { green: { on: { NEXT: "yellow" }, after: { 3500: "yellow", }, }, yellow: { on: { NEXT: "red" }, after: { 1500: "red", }, }, red: { on: { NEXT: "green" }, after: { 5000: "green", }, }, }, }, OFF: { on: { TURN_ON: "ON" }, }, }, });
Let’s break up the changes, starting with our Typestates definitions. We added a case for the OFF state and also modified our color states. They now have a parent ON state, represented by the object notation.
Now to the machine itself, we need to refactor our first level of states definition. We created an ON and an OFF state. The OFF state is a simple state in which we define a single transition. It should transition over to ON when the TURN_ON event is received.
We define states again on the new ON state, with our previous color states and transitions. The lights are now substates of the parent ON states. We also changed the initial state of the entire machine. It now starts with OFF.
As this might be a little intimidating, let’s visualize our machine again to avoid any confusion.
Hierarchical traffic light
React Integration
Now that we’ve made the necessary changes to our state machine, we need to update our react integration to handle the new states and events.
export const App = () => { const [current, send] = useMachine(trafficLightMachine); return (
); };
As you can see, not much has changed; we’ve added two new buttons to turn ON and OFF the traffic light. But the most significant change is how our matches function changes to match the color that should be lit. We need to check if the machine matches the substate of ON, so current.matches({ ON: ‘green’}) will only be true if the machine is ON and substate green.
If we run our application in the browser, we can see that the traffic light starts with all lights off, and we can turn it on to the light cycle using the TURN ON button.
Wrapping up
We’re now starting to see the true power of state machines! We explored how Delayed Transitions can make automatic transitions possible without writing too much code and how the Hierarchical States can help us model more complex behaviors without losing confidence, type safety, and code readability.
You can review the sample code here.