@thomasrandolph/taproot
Advanced tools
+1
-1
| { | ||
| "name": "@thomasrandolph/taproot", | ||
| "version": "0.53.0", | ||
| "version": "0.53.1", | ||
| "description": "It just makes my life a little simpler", | ||
@@ -5,0 +5,0 @@ "type": "module", |
+3
-223
@@ -278,217 +278,2 @@ # taproot | ||
| #### State machines | ||
| taproot Components also ship with a tiny finite state machine attached to them. | ||
| > For more information about why finite state machines are included by default with Components, please refer to: | ||
| > - https://kyleshevlin.com/enumerate-dont-booleanate | ||
| > - https://medium.com/@asolove/pure-ui-control-ac8d1be97a8d | ||
| > - https://css-tricks.com/robust-react-user-interfaces-with-finite-state-machines/ | ||
| > | ||
| > Fundamentally, the reason is: a UI should never be in confusing (or technically impossible) states, and more than one or two possible switches can cause exponential state combination growth. A finite state machine limits both the possible states, and their possible interactions. | ||
| Imagine our Pup component loads the pup dynamically: | ||
| ```js | ||
| import { Component, html } from "https://esm.sh/@thomasrandolph/taproot/Component"; | ||
| export class Pup extends Component{ | ||
| static properties = { | ||
| "pup": { "type": Object }, | ||
| "isLoading": { "type": Boolean, "state": true }, | ||
| "errorLoading": { "type": Boolean, "state": true }, | ||
| "isRetrying": { "type": Boolean, "state": true }, | ||
| "retries": { "type": Number, "state": true } | ||
| } | ||
| constructor(){ | ||
| super(); | ||
| this.pup = null; | ||
| // Internal state | ||
| this.isLoading = false; | ||
| this.errorLoading = false; | ||
| this.isRetrying = false; | ||
| this.retries = 0; | ||
| } | ||
| fetchPup(){ | ||
| var pupUrl = "/api/pup/good-boi-1234"; | ||
| var pup; | ||
| this.isLoading = true; | ||
| this.errorLoading = false; | ||
| try{ | ||
| pup = await fetch( pupUrl ); | ||
| this.isLoading = false; | ||
| this.isRetrying = false; | ||
| this.pup = pup; | ||
| } | ||
| catch( error ){ | ||
| this.errorLoading = true; | ||
| this.isLoading = false; | ||
| this.isRetrying = false; | ||
| if( this.retries < 2 ){ | ||
| ++this.retries; | ||
| this.isRetrying = true; | ||
| this.fetchPup(); | ||
| } | ||
| } | ||
| } | ||
| connectedCallback(){ | ||
| this.fetchPup(); | ||
| } | ||
| render(){ | ||
| var output; | ||
| if( this.loading && !this.retrying ){ | ||
| output = html`Playing fetch with this pup...`; | ||
| } | ||
| else if( this.retrying ){ | ||
| output = html`Had trouble getting this pup to respond to the "Come" command, trying again, a little more firmly.`; | ||
| } | ||
| else if( this.errorLoading ){ | ||
| output = html`Couldn't fetch this pup. ☹️`; | ||
| } | ||
| else if( !this.pup ){ | ||
| output = html`No available pup. ☹️`; | ||
| } | ||
| else{ | ||
| output = html` | ||
| <h1>${this.pup.name}</h1> | ||
| <img src="${this.pup.profilePicture}" /> | ||
| <p>${this.pup.biography}</p> | ||
| `; | ||
| } | ||
| return output; | ||
| } | ||
| } | ||
| ``` | ||
| This is a mess and - to be honest with you - I'm not even sure if it's right. Did I toggle all the booleans correctly? Did I handle each case properly? Do you move between the combined boolean states correctly? I'm not really sure. | ||
| Finite state machines solve this mess by: | ||
| - Defining each possible state | ||
| - Defining exactly how one state moves into others | ||
| - Only allowing those defined transitions | ||
| Let's rewrite with the included state machine: | ||
| ```js | ||
| import { Component, html } from "https://esm.sh/@thomasrandolph/taproot/Component"; | ||
| export class Pup extends Component{ | ||
| static properties = { | ||
| "pup": { "type": Object }, | ||
| "retries": { "type": Number, "state": true } | ||
| } | ||
| constructor(){ | ||
| super(); | ||
| this.pup = null; | ||
| // Internal state | ||
| this.retries = 0; | ||
| this.state.init( { | ||
| "initial": "idle", | ||
| "states": { | ||
| "idle": { | ||
| "on": { | ||
| "FETCH": "loading" | ||
| } | ||
| }, | ||
| "loading": { | ||
| "on": { | ||
| "ERROR": "error", | ||
| "LOADED": "pup" | ||
| } | ||
| }, | ||
| "pup": { | ||
| "on": { | ||
| "FETCH": "loading" | ||
| } | ||
| }, | ||
| "error": { | ||
| "on": { | ||
| "RESET": "idle", | ||
| "RETRY": "retrying", | ||
| } | ||
| }, | ||
| "retrying": { | ||
| "on": { | ||
| "ERROR": "error", | ||
| "LOADED": "pup" | ||
| } | ||
| } | ||
| } | ||
| } ); | ||
| this.state.start(); | ||
| } | ||
| fetchPup(){ | ||
| var pupUrl = "/api/pup/good-boi-1234"; | ||
| var pup; | ||
| this.transition( "FETCH" ); | ||
| try{ | ||
| pup = await fetch( pupUrl ); | ||
| this.transition( "LOADED" ) | ||
| this.pup = pup; | ||
| } | ||
| catch( error ){ | ||
| this.transition( "ERROR" ); | ||
| if( this.retries < 2 ){ | ||
| ++this.retries; | ||
| this.transition( "RETRY" ); | ||
| this.fetchPup(); | ||
| } | ||
| } | ||
| } | ||
| connectedCallback(){ | ||
| this.fetchPup(); | ||
| } | ||
| render(){ | ||
| var output = { | ||
| "idle": html`No available pup. ☹️`, | ||
| "loading": html`Playing fetch with this pup...`, | ||
| "retrying": html`Had trouble getting this pup to respond to the "Come" command, trying again, a little more firmly.`, | ||
| "error": html`Couldn't fetch this pup. ☹️`, | ||
| "pup": html` | ||
| <h1>${this.pup.name}</h1> | ||
| <img src="${this.pup.profilePicture}" /> | ||
| <p>${this.pup.biography}</p> | ||
| ` | ||
| }; | ||
| return output[ this.state ] || html`Unhandled pup state!`; | ||
| } | ||
| } | ||
| ``` | ||
| While the difference in how the finite state machine is integrated into the component is fairly minor, the under-the-hood guarantees are more than worth it: | ||
| 1. Linear - not exponential - potential states | ||
| 2. Impossible to move in an undefined way - each state + transition combination defines exactly how the app can change | ||
| 3. Single-value states rather than boolean combinations - which allows declared behavior lookups rather than imperative tests | ||
| ### Application architecture | ||
@@ -612,7 +397,2 @@ | ||
| this.subscriptions = []; | ||
| this.state.init( { | ||
| // Snipped for brevity, same as before | ||
| } ); | ||
| this.state.start(); | ||
| } | ||
@@ -777,9 +557,9 @@ | ||
| this.transition( transition ); | ||
| this.currentFiniteState = transition; | ||
| }, | ||
| "LOADED_PUP": ( { pup } ) => { | ||
| this.pup = pup; | ||
| this.transition( "LOADED" ); | ||
| this.currentFiniteState = "LOADED"; | ||
| }, | ||
| "LOAD_PUP_FAILURE": () => this.transition( "ERROR" ) | ||
| "LOAD_PUP_FAILURE": () => this.currentFiniteState = "ERROR" | ||
| } ) ); | ||
@@ -786,0 +566,0 @@ } |
51772
-10.93%830
-20.95%