Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@thomasrandolph/taproot

Package Overview
Dependencies
Maintainers
1
Versions
139
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@thomasrandolph/taproot - npm Package Compare versions

Comparing version
0.53.0
to
0.53.1
+1
-1
package.json
{
"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 @@ }