There are many ways to represent state in modern web applications. In React, you can either encapsulate your state locally within components or manage your state globally using a state management library like Redux. Whichever way the state of an app is represented, it is usually represented by components which can be changed to determine how the app is viewed.
When structuring apps, you may be required to have a stylesheet for each form of state your app will be in. Web apps have become increasingly more complex and creating a stylesheet for every form of state in your app could cause complications. In this blog post, we’ll learn how we can use finite state machines to create interactive and complex transitions in our apps.
First off, finite state machines are models that explain the behavior of a system that can only be in one state at a point in time. A state machine’s job is to read a series of inputs and then for each input, switch to a different state. Let’s say you’ve got a car that’s represented by a state machine – it’s either moving, in a state of transition (slowly stopping or slowly starting) or stationary. It can’t be in all four states at the same time and it’s also impossible for it to be neither moving, transitioning between stopping and starting, transitioning between starting and stopping, nor stationary.
When structuring a frontend app, you would have to create events and conditions with event listeners and if
or while
expressions for each different state your app is in. The expansion of your app could see some expressions you make contradict each other. XState lets you implement a model for each state and handles transit between different states.
To show how XState works with frontend frameworks, we’ll build a concept of an interview page with XState and React. This page will render a question and a sidebar that gives a user the chance to peek at the question’s answer. At any given point in time, a user is either:
For animation, and including CSS styles in our components, we’ll make use of the following:
Assuming we’ve created a React app (see how to do that here), we’ll install both XState
and xstate/react
, a set of utilities for using XState in React apps:
npm i xstate @xstate/react
The first component we’ll create is a button for our transitions. In this button component, we’ll pass down props from our main component. Which will contain our state machine as well as our invoked functions:
// Button.jsx /** @jsx jsx */ import { css, jsx } from "@emotion/core"; export const Button = props => ( <button {...props} > {props.children} </button> );
Next, we’ll create a Test
component which will house the body of our app as well as functions that can be called by our state machine during its transition process. Let’s define two functions – openAnswer
and closeAnswer
for switching between the questions and answers:
// Test.jsx const openAnswer = useCallback( (context, event) => { return new Promise(resolve => { TweenMax.to(element.current, 0.5, { x: 0, backdropFilter: "blur(2px)", ease: Elastic.easeOut.config(1, 1), onComplete: resolve }); }); }, [element] ); const closeAnswer = useCallback( (context, event) => { return new Promise(resolve => { TweenMax.to(element.current, 0.5, { x: -290, backdropFilter: "blur(0px)", ease: Elastic.easeOut.config(1, 1), onComplete: resolve }); }); }, [element] );
In the code sample above, we used the useCallback
Hook to keep track of changes and only update if a value has changed to prevent unnecessary re-renders. The image below depicts that state of the app when the Hook in openAnswer
is triggered:
And a similar image showing when the Hook in closeAnswer
is triggered:
We’ll specify all four states our app can be in with the initial state as the view of an interview question:
<!--Test.jsx --> const testMachine = Machine({ initial: 'QUESTIONS', states: { QUESTIONS: {}, VIEWING: {}, ANSWERS: {}, RETURNING: {}, }, })
Next, we’ll configure in our state machine, the transition path we want our app to take. We’ll go from QUESTIONS
to ANSWERS
with VIEWING
and RETURNING
acting as transitions:
<!--Test.jsx --> const testMachine = Machine({ initial: 'QUESTIONS', states: { QUESTIONS: { on: { ANSWERS: 'VIEWING', }, }, VIEWING: { on: { REPEAT: 'RETURNING', }, }, ANSWERS: { on: { REPEAT: 'RETURNING', }, }, RETURNING: { on: { ANSWERS: 'VIEWING', }, }, }, })
To complete our state machine, we need to properly handle the transitions that occur in-between the VIEWING
and RETURNING
states. With its invoke property, XState has the ability to create functions that can return a promise. Let’s add the functions we earlier defined for switching between questions and answers:
<!--Test.jsx --> const testMachine = Machine({ initial: "QUESTIONS", states: { QUESTIONS: { on: { ANSWERS: "VIEWING" } }, VIEWING: { invoke: { src: "openAnswer", onDone: { target: "ANSWERS" } }, on: { REPEAT: "RETURNING" } }, ANSWERS: { on: { REPEAT: "RETURNING" } }, RETURNING: { invoke: { src: "closeAnswer", onDone: { target: "QUESTIONS" } }, on: { ANSWERS: "VIEWING" } } } });
Now we’re done defining our state, we’ll use the useMachine
Hook to configure our state machine’s services. This exposes our app’s current state and the possible state it can be set to:
<!--Test.jsx --> const [current, send] = useMachine(testMachine, { services: { openAnswer, closeAnswer } });
Next, we need to set our user interface to communicate with the state and controls we’ve created so far. In Test.jsx
, set a ternary operator to choose what the view should be on the main page when the ANSWERS
button is clicked:
<!--Test.jsx --> const testAnswers = current.matches("ANSWERS") || current.matches("VIEWING") ? "REPEAT" : "ANSWERS"; let label = testAnswers === "ANSWERS" ? "ANSWERS" : "REPEAT";
Next, we’ll create our button. When clicked, it should generate transitions that align with the ternary operator we just defined:
<!-- Test.jsx --> return ( <div ref={element} css={css` color: #fff; z-index: 9999; position: absolute; top: 0; bottom: 0; left: 0; width: 380px; transform: translateX(-290px); display: grid; grid-template-rows: 40px auto; align-content: start; justify-content: end; `} > <Button onClick={() => { send(testAnswers); }} > {label} </Button> </div> );
In the clip below, the header that shows the state of that app transitions between both states as the button – whose state also changes according to what we set in the ternary operator – is clicked:
Screen Recording 2020 04 12 at 13 34 47
Uploaded by Raphael Ugwu on 2020-04-12.
Lastly, we’ll create a functional parent component that handles the multiple constants in our Test
component and returns the starting state of our app via a useState
Hook:
<!-- index.js --> import { css, jsx } from "@emotion/core"; import { useState } from "react"; import { Test } from "./Test"; function App() { const [testStatus, setTestStatus] = useState(); return ( <div> <Test setStatus={setTestStatus} /> <div> <div> {testStatus} </div> <div> <span role="img" aria-label="paper"> đź“ť </span> The inner diameter of a test tube can be measured accurately using a: <br></br> </div> </div> </div> ); }
We’re finally done! Here’s the finished app, note the simultaneous switching of states in all the parts involved:
Screen Recording 2020 04 01 at 17 05 16
Uploaded by Raphael Ugwu on 2020-04-01.
There’s no uniform solution when it comes to handling state in web applications. Just be sure you’ve got your architecture right and the conditions you create in each component do not contradict. XState helps in making these conditions specific. Built around the concept of finite state machines which have been around for some time, it’s very stable. Should you like to understand the code samples more, you can view a full working demo here on CodeSandbox.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.