driver

A typescript package for declaring finite states and commonly derived value...

README

🏁 driver


driver is a tiny typescript utility for organizing external data into finite states and deriving common values.

Jump to sample code or the docs. Get help & support in the Discord.

✨ Features


- Tiny with zero dependencies (<500B _gzipped + minified_)
- Framework agnostic (works with react, svelte, vue, node, deno, bun, cloudflare workers, etc.)
- Fully typed
- Declarative API
- Readable source code (~60 lines including comments)

📦 Installation


  1. ```bash
  2. $ npm i @switz/driver
  3. ```

🍬 Sample Code


  1. ```javascript
  2. import driver from '@switz/driver';

  3. const CheckoutButton = ({ cartData, isLoading, checkout }) => {
  4.   const shoppingCart = driver({
  5.     // the first state to return true is the active state
  6.     states: {
  7.       isLoading,
  8.       isCartEmpty: cartData.items.length === 0,
  9.       isCartInvalid: !!cartData.isError,
  10.       isCartValid: true, // fallback/default
  11.     },
  12.     derived: {
  13.       // arrays resolve to a boolean (true) if the active state
  14.       // matches a state key in the array
  15.       isDisabled: ['isLoading', 'isCartEmpty', 'isCartInvalid'],
  16.       // objects resolve to whichever value is specified as
  17.       // the currently active state
  18.       intent: {
  19.         isLoading: 'info',
  20.         isCartEmpty: 'info',
  21.         isCartInvalid: 'error',
  22.         isCartValid: 'primary',
  23.       },
  24.     },
  25.   });

  26.   return (
  27.     <Button
  28.       icon="checkout"
  29.       intent={shoppingCart.intent}
  30.       disabled={shoppingCart.isDisabled}
  31.       onClick={checkout}
  32.     >
  33.       Checkout
  34.     </Button>
  35.   );
  36. }
  37. ```

If isLoading is the active state:

  1. ```js
  2. shoppingCart.isDisabled => true
  3. shoppingCart.intent => 'info'
  4. ```

Similarly, if isCartValid is the active state:

  1. ```js
  2. shoppingCart.isDisabled => false
  3. shoppingCart.intent => 'primary'
  4. ```

👩‍🏭 Basic Introduction


Each driver works by defining finite states. Only one state can be active at any given time. The first state to resolve to true is active.

Let's look at some examples. I'm going to use React, but you don't have to.

We define the possible states in the states object. The first state value to be true is the active state (these are akin to if/else statements).

  1. ```javascript
  2. import driver from '@switz/driver';

  3. const CheckoutButton = ({ cartData }) => {
  4.   const button = driver({
  5.     states: {
  6.       isEmpty: cartData.items.length === 0,
  7.       canCheckout: cartData.items.length > 0,
  8.     },
  9.     derived: {
  10.       // if the active state matches any strings in the array, `isDisabled` returns true
  11.       isDisabled: ['isEmpty'],
  12.     },
  13.   });

  14.   return (
  15.     <Button icon="checkout" disabled={button.isDisabled} onClick={onClick}>
  16.       Checkout
  17.     </Button>
  18.   );
  19. }
  20. ```

Since driver gives us some guardrails to our stateful logic, they can be reflected as state tables:

StatesisDisabled
--------------------------
isEmptytrue
canCheckoutfalse

Here we have two possible states: isEmpty or canCheckout and one derived value from each state: isDisabled.

Now you're probably thinking – this is over-engineering! We only have two states, why not just do this:

  1. ```javascript
  2. const CheckoutButton = ({ cartItems }) => {
  3.   const isEmpty = cartItems.length === 0;

  4.   return (
  5.     <Button icon="checkout" disabled={isEmpty} onClick={onClick}>
  6.       Checkout
  7.     </Button>
  8.   );
  9. }
  10. ```

And in many ways you'd be right. But as your logic and code grows, you'll very quickly end up going from a single boolean flag to a mishmash of many. What happens when we add a third, or fourth state, and more derived values? What happens when we nest states? You can quickly go from 2 possible states to perhaps 12, 24, or many many more even in the simplest of components.

Here's a more complex example with 4 states and 3 derived values. Can you see how giving our state some rigidity could reduce logic bugs?

  1. ```javascript
  2. const CheckoutButton = ({ cartItems, isLoading, checkout }) => {
  3.   const cartValidation = validation(cartItems);
  4.   const shoppingCart = driver({
  5.     states: {
  6.       isLoading,
  7.       isCartEmpty: cartItems.length === 0,
  8.       isCartInvalid: !!cartValidation.isError,
  9.       isCartValid: true, // fallback/default
  10.     },
  11.     derived: {
  12.       popoverText: {
  13.         // unspecified states (isLoading, isCartValid here) default to undefined
  14.         isCartEmpty: 'Your shopping cart is empty, add items to checkout',
  15.         isCartInvalid: 'Your shopping cart has errors: ' + cartValidation.errorText,
  16.       },
  17.       buttonVariant: {
  18.         isLoading: 'info',
  19.         isCartEmpty: 'info',
  20.         isCartInvalid: 'error',
  21.         isCartValid: 'primary',
  22.       },
  23.       // onClick will be undefined except `ifCartValid` is true
  24.       //
  25.       onClick: {
  26.         isCartValid: checkout,
  27.       }
  28.     },
  29.   });

  30.   return (
  31.     <Popover content={shoppingCart.popoverText} disabled={!shoppingCart.popoverText}>
  32.       <Button icon="checkout" intent={shoppingCart.buttonVariant} disabled={!shoppingCart.onClick} onClick={shoppingCart.onClick}>
  33.         Checkout
  34.       </Button>
  35.     </Popover>
  36.   );
  37. }
  38. ```

What does this state table look like?

StatespopoverTextbuttonVariantonClick
----------------------------------------------------
isLoading||
isCartEmpty"Yourinfo|
isCartInvalid"Yourerror|
isCartValid|()

Putting it in table form displays the rigidity of the logic that we're designing.

🖼️ Background


After working with state machines, I realized the benefits of giving your state rigidity. I noticed that I was tracking UI states via a plethora of boolean values, often intermixing const/let declarations with inline ternary logic. This is often inevitable when working with stateful UI libraries like react.

Even though state machines are very useful, I also realized that my UI state is largely derived from boolean logic (via API data or React state) and not from a state machine I want to build and manually transition myself. So let's take out the machine part and just reflect common stateful values.

For example, a particular button component may have several states, but will always need to know:

1. is the button disabled/does it have an onClick handler?
2. what is the button text?
3. what is the button's style/variant/intent, depending on if its valid or not?

and other common values like

4. what is the popover/warning text if the button is disabled?

By segmenting our UIs into explicit states, we can design and extend our UIs in a more pragmatic and extensible way. Logic is easier to reason about, organize, and test – and we can extend that logic without manipulating inline ternary expressions or fighting long lists of complex boolean logic.

Maybe you have written (or had to modify), code that looks like this:

  1. ```javascript
  2. const CheckoutButton = ({ cartItems, isLoading }) => {
  3.   const cartValidation = validation(cartItems);

  4.   let popoverText = 'Your shopping cart is empty, add items to checkout';
  5.   let buttonVariant = 'info';
  6.   let isDisabled = true;

  7.   if (cartValidation.isError) {
  8.     popoverText = 'Your shopping cart has errors: ' + cartValidation.errorText;
  9.     buttonVariant = 'error';
  10.   }
  11.   else if (cartValidation.hasItems) {
  12.     popoverText = null;
  13.     isDisabled = false;
  14.     buttonVariant = 'primary';
  15.   }

  16.   return (
  17.     <Popover content={popoverText} disabled={!popoverText}>
  18.       <Button icon="checkout" intent={buttonVariant} disabled={isLoading || isDisabled} onClick={checkout}>
  19.         Checkout
  20.       </Button>
  21.     </Popover>
  22.   );
  23. }
  24. ```

Touching this code is a mess, keeping track of the state tree is hard, and interleaving state values, boolean logic, and so on is cumbersome. You could write this a million different ways.

Not to mention the implicit initial state that the default values imply the cart is empty. This state is essentially hidden to anyone reading the code. You could write this better – but you could also write it even worse. By using driver, your states are much more clearly delineated.


Other examples:


Every _driver_ contains a single active state. The first key in states to be true is the active state.

  1. ```javascript
  2. const DownloadButton = ({ match }) => {
  3.   const demoButton = driver({
  4.     states: {
  5.       isNotRecorded: !!match.config.dontRecord,
  6.       isUploading: !match.demo_uploaded,
  7.       isUploaded: !!match.demo_uploaded,
  8.     },
  9.     derived: {
  10.       isDisabled: ['isNotRecorded', 'isUploading'],
  11.       // could also write this as:
  12.       // isDisabled: (states) => states.isNotRecorded || states.isUploading,
  13.       text: {
  14.         isNotRecorded: 'Demo Disabled',
  15.         isUploading: 'Demo Uploading...',
  16.         isUploaded: 'Download Demo',
  17.       },
  18.     },
  19.   });

  20.   return (
  21.     <Button icon="download" disabled={!!demoButton.isDisabled}>
  22.       {demoButton.text}
  23.     </Button>
  24.   );
  25. }
  26. ```

The derived data is pulled from the state keys. You can pass a function (and return any value), an array to mark boolean derived flags, or you can pass an object with the state keys, and whatever the current state key is will return that value.

isDisabled is true if any of the specified state keys are active, whereas text returns whichever string corresponds directly to the currently active state value.

Now instead of tossing ternary statements and if else and tracking messy declarations, all of your ui state can be derived through a simpler and concise state-machine inspired pattern.

The goal here is not to have _zero_ logic inside of your actual view, but to make it easier and more maintainable to design and build your view logic in some more complex situations.

👾 Docs


The driver function takes an object parameter with two keys: states and derived.

  1. ```javascript
  2. driver({
  3.   states: {
  4.     state1: false,
  5.     state2: true,
  6.   },
  7.   derived: {
  8.     text: {
  9.       state1: 'State 1!',
  10.       state2: 'State 2!',
  11.     }
  12.   }
  13. })
  14. ```

states is an object whose keys are the potential state values. Passing dynamic boolean values into these keys dictates which state key is currently active. The first key with a truthy value is the active state.

derived is an object whose keys derive their values from what the current state key is. There are three interfaces for the derived object.

States


  1. ```javascript
  2. driver({
  3.   states: {
  4.     isNotRecorded: match.config.dontRecord,
  5.     isUploading: !match.demo_uploaded,
  6.     isUploaded: match.demo_uploaded,
  7.   },
  8. });
  9. ```

Derived


Function


You can return any value you'd like out of the function using the state keys

  1. ```diff
  2. driver({
  3.   states: {
  4.     isNotRecorded: match.config.dontRecord,
  5.     isUploading: !match.demo_uploaded,
  6.     isUploaded: match.demo_uploaded,
  7.   },
  8. +  derived: {
  9. +    isDisabled: (states) => states.isNotRecorded || states.isUploading,
  10. +  }
  11. })
  12. ```

or you can access generated enums for more flexible logic


  1. ```diff
  2. driver({
  3.   states: {
  4.     isNotRecorded: match.config.dontRecord,
  5.     isUploading: !match.demo_uploaded,
  6.     isUploaded: match.demo_uploaded,
  7.   },
  8.   derived: {
  9. +   isDisabled: (_, stateEnums, activeEnum) => (activeEnum ?? 0) <= stateEnums.isUploading,
  10.   }
  11. })
  12. ```

This declares that any state key _above_ isUploaded means the button is disabled (in this case, isNotRecorded and isUploading). This is useful for when you have delinated states and you want to more dynamically define where those lines are.

Array


By using an array, you can specify a boolean if any item in the array matches the current state:


  1. ```diff
  2. driver({
  3.   states: {
  4.     isNotRecorded: match.config.dontRecord,
  5.     isUploading: !match.demo_uploaded,
  6.     isUploaded: match.demo_uploaded,
  7.   },
  8.   derived: {
  9. +   isDisabled: ['isNotRecorded', 'isUploading'],
  10.   }
  11. })
  12. ```

This returns true if the active state is: isNotRecorded or isUploading.

This is the same as writing: (states) => states.isNotRecorded || states.isUploading in the function API above.

Object Lookup


If you want to have an independent value per active state, an object map is the easiest way. Each state key returns its value if it is the active state. For Example:


  1. ```diff
  2. driver({
  3.   states: {
  4.     isNotRecorded: match.config.dontRecord,
  5.     isUploading: !match.demo_uploaded,
  6.     isUploaded: match.demo_uploaded,
  7.   },
  8.   derived: {
  9. +   text: {
  10. +     isNotRecorded: 'Demo Disabled',
  11. +     isUploading: 'Demo Uploading...',
  12. +     isUploaded: 'Download Demo',
  13. +   },
  14.   }
  15. })
  16. ```

If the current state is isNotRecorded then the text key will return 'Demo Disabled'.

isUploading will return 'Demo Uploading...', and isUploaded will return 'Download Demo'.


Svelte Example


This is a button with unique text that stops working at 10 clicks. Just prepend the driver call with $: to mark it as reactive.

  1. ```javascript
  2. <script>
  3.     import driver from "@switz/driver";
  4.     let count = 0;

  5.     function handleClick() {
  6.       count += 1;
  7.     }

  8.     // use $ to mark our driver as reactive
  9.     $: buttonInfo = driver({
  10.       states: {
  11.         IS_ZERO: count === 0,
  12.         IS_TEN: count >= 10,
  13.         IS_MORE: count >= 0
  14.       },
  15.       derived: {
  16.         text: {
  17.           IS_ZERO: "Click me to get started",
  18.           IS_MORE: `Clicked ${count} ${count === 1 ? "time" : "times"}`,
  19.           IS_TEN: "DONE!"
  20.         },
  21.         isDisabled: ["IS_TEN"]
  22.       }
  23.     });
  24. </script>

  25. <button on:click={handleClick} disabled={buttonInfo.isDisabled}>
  26.   {buttonInfo.text}
  27. </button>
  28. ```

Key Ordering Consistency


My big concern here was abusing the ordering of object key ordering. Since the order of your `states object matters, I was worried that javascript may not respect key ordering.

According to: https://stackoverflow.com/questions/5525795/does-javascript-guarantee-object-property-order/38218582#38218582

Property order in normal Objects is a complex subject in JavaScript.

>

While in ES5 explicitly no order has been specified, ES2015 defined an order in certain cases, and successive changes to the specification since have increasingly defined the order (even, as of ES2020, the for-in loop's order).

>

This results in the following order (in certain cases):

>

Object {

0: 0,

1: "1",

2: "2",

b: "b",

a: "a",

m: function() {},

Symbol(): "sym"

}

The order for "own" (non-inherited) properties is:

>

Positive integer-like keys in ascending order

String keys in insertion order

Symbols in insertion order

>

https://tc39.es/ecma262/#sec-ordinaryownpropertykeys


Due to this, we force you to define your states keys as strings and only strings. This should prevent breaking the ordering of your state keys in modern javascript environments.

If you feel this is wrong, please open an issue and show me how we can improve it.

Help and Support


Join the Discord for help: https://discord.gg/dAKQQEDg9W

Warning: this is naive and changing


This is still pretty early, the API surface may change. Code you write with this pattern may end up being _less_ efficient than before, with the hope that it reduces your logic bugs. This code is not _lazy_, so you may end up evaluating far more than you need for a given component. In my experience, you should not reach for a driver _immediately_, but as you see it fitting in, use it where it is handy. The _leafier_ the component (meaning further down the tree, closer to the bottom), the more useful I've found it.

Typescript


This library is fully typed end-to-end. That said, this is the first time I've typed a library of this kind and it could definitely be improved. If you run into an issue, please raise it or submit a PR.

Local Development


To install dependencies:

  1. ```bash
  2. bun install
  3. ```

To test:

  1. ```bash
  2. npm run test # we test the typescript types on top of basic unit tests
  3. ```