erikras.com
HomeAbout

Modeling React in XState

Can XState state charts improve your understanding of the React render cycle?

Posted in Coding, React, XState
June 24, 2022 - 4 min read
React Lifecycle Methods
React Lifecycle Methods

The chart above is from a famous tweet by Dan Abramov in 2018. Dan, as usual, does an amazing job of communicating his deep technical knowledge in a way that is easy to comprehend.

Since the release of Stately's Visual Editor, I've been using it more and more at my dayjob at Centered to construct new, and comprehend existing, state machines in our codebase. The visual editor is just so fun to use!

The other day, I thought to myself,

"I wonder if I could test my knowledge of React and state charts, and model the React lifecycle methods in a state chart?" 🤔

So I gave it a try!

React Lifecycle State Chart

React Lifecycle State ChartElegant, ain't it?

One thing that immediately jumped out at me was how all the lifecycle methods – and render() itself! – ended up being side effects, or "actions" in XState parlance. This makes total sense, as they really are where you put side effects!

Rendering is a side effect!

The Code

This is the XState machine that the visual editor generated:

import { createMachine } from "xstate";
export default /** @xstate-layout N4IgpgJg5mDOIC5QCUwEMDGAXABAYQHsBbABwIDsxysA6AWQIFdrIBiAVXKKesVDNgBLLIIp8QAD0QBaAIwAGAGw0ALAE4AHPICsigMxrFi+RqMAaEAE8ZAJhsaaAdm0qVsmyb12bamwF8-C1RMXEJSCipaBmYsNgA5MAB3HAAFACcCEngkEAFhUXJxKQRpNxsaeVk9FUc1Xz09DTttC2sS-WUG9wbtbUdXWUcAoPRsfGIySmp6HliIVgBlMFwFrDRY8TyRMRzi2SqaRTVHE3t+tS7HVplFDxofBUUVDQ1ZC5tZYZBgsbDJyJmMTYADECGkMGAcOwSBB1mBNgQhNtCrsZAp5DRZC4zvI9I4DPtriVtC8aGoVDZtLImoZHI4ngFAiByAQIHBxD9QhMItNoiwIDRUOQ2WlIAikQUijJapj8U19iSVLpZCoidI9L0KvJ5EqdQTNEovpzxuEplFZmKcltJaiSio9MpKtVavVGs01S8MV07I4fJoLiojaMuaaAZxuECIOL8jtQHs8WT5fI7KY3eYrLYsU4XColIZFNofP4mca-jysNHkVK7cYKlUanUbA0mpS1XUvXp3DZatVc7JFIy-EA */
createMachine({
description: "made by @erikras",
initial: "Mounted",
states: {
Mounted: {
entry: [
"constructor",
"getDerivedStateFromProps",
"render",
"componentDidMount",
],
initial: "Rendered",
states: {
Rendered: {},
},
on: {
Unmount: {
actions: "componentWillUnmount",
target: "Unmounted",
},
"New Props": {
actions: ["getDerivedStateFromProps", "render", "componentDidUpdate"],
cond: "shouldComponentUpdate",
target: ".Rendered",
},
"Set State": {
actions: ["render", "componentDidUpdate"],
cond: "shouldComponentUpdate",
target: ".Rendered",
},
"Force Update": {
actions: ["render", "componentDidUpdate"],
target: ".Rendered",
},
},
},
Unmounted: {
type: "final",
},
},
id: "React Component",
});

The crazy comment at the top is how the visual editor encodes the location of all the boxes and arrows and whatnot, so if you open this exact file in the editor, the layout will be the same. Clever, huh?

Of course, because I tweeted it, I got a clever response/challenge, from my friend, Donovan:

Nice. Now do this one.

with a link to this chart he, his son, and Dan put together three years ago:

React Hooks LifecycleReact Hooks Lifecycle

I couldn't turn down such a challenge, now could I?

React Hooks State Chart

It actually didn't take me long at all to whip up this one. Although React hooks, as an API, is full of footguns with which to shoot yourself, the actual mental model of what is happening is simpler than the lifecycle methods.

React Hooks State ChartReact Hooks State Chart

With this one, I took the additional step of imagining we were actually implementing it as an XState state machine, where we could have to save the props, state, and context into the machine's context (that's the XState term for the internal storage of the machine).

The Code

import { assign, createMachine } from "xstate";
export default /** @xstate-layout N4IgpgJg5mDOIC5QCUwEMDGAXABAYQHsBbABwIDsxzdBkAhwAkCCBrWHAMQBsCB3AOgCyBAK7VIfVOQhgATpADEABRkESbPAAs05GIlBlYASyyGKekAA9EAWgAsAdgAMfewGYAnK9e3XARgBs7v6OrgA0IACeNvYATPZ8tgAcMX7BjgH+9r4AvtnhqJi4hKQUVLQMTKwc3PxColjiktJyEPIAylhoDfhaOmDmBsam5OZWCHb+iXzuvgCsvr6J7rY+7u724VHj-r7u0zGJib5uwa7+B7n56Nj4xGSU1Dh0jCxsXLyCImIQElTNCoQxBYir1dEgQIMTGZwWM7PYpnEVolzjFbLNXEdNjZ5lMfPYso5-LZHGtZjFLiACjdivcyk8Kq9qh86t95ABVchEL5YAYEIxQkYwxD2WZ8SaxWKuRwxYJJXxY7Zk6azFU7HZJFX2XJ5EDkAjSeDgqlFO6lR7PSpvGqfeqNP6ySC8-nDUY2A6uaZHEW+Ry+1zo1wbSLRGIxBIHI7pUNrGUU423EoPcovKrvWrcx3gyEuoXjWbSviIpIotEY+XB8aJeYJZKpX0ZLJx64mxN0i2MtN8Dlc20QJ1DaGgMa2fx8ZFSvy+Ym+QOzWwK6yzI58RxkxyJJKuGJT3ZNwoJ2nmhmpmr9gWuvMywv2JEl9GYivWIK+GspUMi-xz-za7JAA */
createMachine(
{
description: "made by @erikras",
initial: "Mounted",
context: {
props: {},
state: {},
context: {},
},
states: {
Mounted: {
entry: "Run Lazy Initializers",
initial: "Rendered",
states: {
Rendered: {
entry: [
"Render",
"Cleanup Layout Effects",
"Run Layout Effects",
"Browser Paints Screen",
"Cleanup Effects",
"Run Effects",
],
on: {
"Props Change": {
actions: "setProps",
},
"State Change": {
actions: "setState",
},
"Context Change": {
actions: "setContext",
},
},
},
},
on: {
Unmount: {
target: "Unmounted",
},
},
},
Unmounted: {
type: "final",
},
},
id: "React Component – Hooks Flow",
},
{
actions: {
setContext: assign({ props: (_context, event) => event.props }),
setProps: assign({ props: (_context, event) => event.props }),
setState: assign({ props: (_context, event) => event.props }),
},
}
);

Conclusion

Am I trying to reimplement React using state machines? Hell no!

But this exercise has served to benefit my understanding – and maybe yours too? – of how React works, how to model flows in state charts using Stately's visual editor, and how to read and understand state charts.

Discuss on Twitter

© 2023 Erik Rasmussen