GithubHelp home page GithubHelp logo

thefrontside / microstates Goto Github PK

View Code? Open in Web Editor NEW
1.3K 22.0 53.0 11.01 MB

Composable state primitives for JavaScript

JavaScript 100.00%
composition state-machines lens microstates transition batch-transitions

microstates's Introduction

npm bundle size (minified + gzip) Build Status Coverage Status License: MIT Chat on Discord Created by The Frontside

Microstates Logo
Microstates

Microstates makes working with pure functions over immutable data feel like working with the classic, mutable models we all know and love.

Table of Contents
๐ŸŽฌ Videos
๐Ÿ’ฌ Chat

Join our community on Discord. Everyone is welcome. If you're a new to programming join our #beginner channel where extra care is taken to support those who're just getting started.

Features

With Microstates added to your project, you get:

  • ๐Ÿ‡ Composable type system
  • ๐Ÿฑ Reusable state atoms
  • ๐Ÿ’Ž Pure immutable state transitions without writing reducers
  • โšก๏ธ Lazy and synchronous out of the box
  • ๐Ÿฆ‹ Most elegant way to express state machines
  • ๐ŸŽฏ Transpilation free type system
  • ๐Ÿ”ญ Optional integration with Observables
  • โš› Use in Node.js, browser or React Native
  • ๐Ÿ”ฌ It's tiny

But, most importantly, Microstates makes working with state fun.

When was the last time you had fun working with state?

For many, the answer is probably never, because state management in JavaScript is an endless game of compromises. You can choose to go fully immutable and write endless reducers. You can go mutable and everything becomes an observable. Or you can setState and lose the benefits of serialization and time travel debugging.

Unlike the view layer, where most frameworks agree on some variation of React's concept of components, none of the current crop of state management tools strike the same balance that React components introduced to the API.

React components have a tiny API. They are functional, simple and extremely reusable. The tiny API gives you high productivity for little necessary knowledge. Functional components are predictable and easy to reason about. They are conceptually simple, but simplicity hides an underlying architecture geared for performance. Their simplicity, predictability and isolation makes them composable and reusable.

These factors combined are what make React style components easy to work with and ultimately fun to write. A Tiny API abstracting a sophisticated architecture that delivers performance and is equally useful on small and big projects is the outcome that we set out to achieve for state management with Microstates.

It's not easy to find the right balance between simplicity and power, but considering the importance of state management in web applications, we believe it's a worthy challenge. Checkout the Vision of Microstates section if you're interested in learning more about where we're going.

What is a Microstate?

A Microstate is just an object that is created from a value and a type. The value is just data, and the type is what defines how you can transition that data from one form into the next. Unlike normal JavaScript objects, microstates are 100% immutable and cannot be changed. They can only derive new immutable microstates through one of their type's transitions.

Types and Type Composition

Microstates comes out of the box with 5 primitive types: Boolean, Number, String, Object and Array.

import { create, valueOf } from 'microstates';

let meaningOfLifeAndEverything = create(Number, 42);
console.log(meaningOfLifeAndEverything.state);
//> 42

let greeting = create(String, 'Hello World');
console.log(greeting.state);
//> Hello World

let isOpen = create(Boolean, true);
console.log(isOpen.state);
//> true

// For Object and Array use microstates valueOf method
let foo = create(Object, { foo: 'bar' });
console.log(valueOf(foo));
//> { foo: 'bar' }

let numbers = create(Array, [1, 2, 3, 4]);
console.log(valueOf(numbers));
//> [ 1, 2, 3, 4 ]

Apart from these basic types, every other type in Microstates is built by combining other types. So for example, to create a Person type you could define a JavaScript class with two properties: name which has type String and age which has type Number.

class Person {
  name = String;
  age = Number;
}

Once you have a type, you can use that type to create as many people as your application requires:

import { create } from 'microstates';

let person = create(Person, { name: 'Homer', age: 39 });

Every microstate created with a type of Person will be an object extending Person to have a set() method:

+----------------------+
|                      |       +--------------------+
|  Microstate<Person>  +-name--+                    +-concat()->
|                      |       | Microstate<String> +-set()->
|                      |       |                    +-state: 'Homer'
|                      |       +--------------------+
|                      |
|                      |       +--------------------+
|                      +-age---+                    +-increment()->
|                      |       | Microstate<Number> +-decrement()->
|                      |       |                    +-set()->
|                      |       |                    +-state: 39
|                      |       +--------------------+
|                      |
|                      +-set()->
|                      |
+----------------------+

For the five built in types, Microstates automatically gives you transitions that you can use to change their value. You don't have to write any code to handle common operations.

Creating your own microstates

Types can be combined with other types freely and Microstates will take care of handling the transitions for you. This makes it possible to build complex data structures that accurately describe your domain.

Let's define another type that uses the person type.

class Car {
  designer = Person;
  name = String;
}

let theHomerCar = create(Car, {
  designer: { name: 'Homer', age: 39 },
  name: 'The Homer'
});

theHomerCar object will have the following shape,

+-------------------+           +----------------------+
|                   |           |                      |       +--------------------+
|  Microstate<Car>  |           |  Microstate<Person>  +-name--+                    +-concat()->
|                   |           |                      |       | Microstate<String> +-set()->
|                   +-designer--+                      |       |                    +-state: 'Homer'
|                   |           |                      |       +--------------------+
|                   |           |                      |
|                   |           |                      |       +--------------------+
|                   |           |                      +-age---+                    +-increment()->
|                   |           |                      |       | Microstate<Number> +-decrement()->
|                   |           |                      |       |                    +-set()->
|                   |           |                      |       |                    +-state: 39
|                   |           |                      |       +--------------------+
|                   |           |                      |
|                   |           |                      +-set()->
|                   |           |                      |
|                   |           +----------------------+
|                   |
|                   |           +--------------------+
|                   +-name------+                    +-concat()->
|                   |           | Microstate<String> +-set()->
|                   |           |                    +-state: 'The Homer'
|                   |           +--------------------+
|                   |
|                   +-set()->
|                   |
+-------------------+

You can use the object dot notation to access sub microstates. Using the same example from above:

theHomerCar.designer;
//> Microstate<Person>{ name: Microstate<String>'Homer', age: Microstate<Number>39 }

theHomerCar.designer.age.state;
//> 39

theHomerCar.name.state;
//> The Homer

You can use the valueOf() function available from the microstates module to retrieve the underlying value represented by a microstate.

import { valueOf } from 'microstates';

valueOf(theHomerCar);
//> { designer: { name: 'Homer', age: 39 }, name: 'The Homer' }

Array Microstates

Quite often it is helpful to describe your data as a collection of types. For example, a blog might have an array of posts. To do this, you can use the array of type notation [Post]. This signals that Microstates of this type represent an array whose members are each of the Post type.

class Blog {
  posts = [Post];
}

class Post {
  id = Number;
  title = String;
}

let blog = create(Blog, {
  posts: [
    { id: 1, title: 'Hello World' },
    { id: 2, title: 'Most fascinating blog in the world' }
  ]
});

for (let post of blog.posts) {
  console.log(post);
}
//> Microstate<Post>{ id: 1, title: 'Hello World' }
//> Microstate<Post>{ id: 2, title: 'Most fascinating blog in the world' }

When you're working with an array microstate, the shape of the Microstate is determined by the value. In this case, posts is created with two items which will, in turn, create a Microstate with two items. Each item will be a Microstate of type Post. If you push another item onto the posts Microstate, it'll be treated as a Post.

let blog2 = blog.posts.push({ id: 3, title: 'It is only getter better' });

for (let post of blog2.posts) {
  console.log(post);
}

//> Microstate<Post>{ id: 1, title: 'Hello World' }
//> Microstate<Post>{ id: 2, title: 'Most fascinating blog in the world' }
//> Microstate<Post>{ id: 3, title: 'It is only getter better' }

Notice how we didn't have to do any extra work to define the state transition of adding another post to the list? That's the power of composition!

Object Microstates

You can also create an object microstate with {Post}. The difference is that the collection is treated as an object. This can be helpful when creating normalized data stores.

class Blog {
  posts = { Post };
}

class Post {
  id = Number;
  title = String;
}

let blog = create(Blog, {
  posts: {
    '1': { id: 1, title: 'Hello World' },
    '2': { id: 2, title: 'Most fascinating blog in the world' }
  }
});

blog.posts.entries['1'];
//> Microstate<Post>{ id: 1, title: 'Hello World' }

blog.posts.entries['2'];
//> Microstate<Post>{ id: 2, title: 'Most fascinating blog in the world' }

Object type microstates have Object transitions, such as assign, put and delete.

let blog2 = blog.posts.put('3', { id: 3, title: 'It is only getter better' });

blog2.posts.entries['3'];
//> Microstate<Post>{ id: 3, title: 'It is only getter better' }

Transitions

Transitions are the operations that let you derive a new state from an existing state. All transitions return another Microstate. You can use state charts to visualize microstates. For example, the Boolean type can be described with the following statechart.

Boolean Statechart

The Boolean type has a toggle transition which takes no arguments and creates a new microstate with the state that is opposite of the current state.

Here is what this looks like with Microstates.

import { create } from 'microstates';

let bool = create(Boolean, false);

bool.state;
//> false

let inverse = bool.toggle();

inverse.state;
//> true

Pro tip Remember, Microstate transitions always return a Microstate. This is true both inside and outside the transition function. Using this convention can allow composition to reach crazy levels of complexity.

Let's use a Boolean in another type and see what happens.

class App {
  name = String;
  notification = Modal;
}

class Modal {
  text = String;
  isOpen = Boolean;
}

let app = create(App, {
  name: 'Welcome to your app',
  notification: {
    text: 'Hello there',
    isOpen: false
  }
});

let opened = app.notification.isOpen.toggle();
//> Microstate<App>

valueOf(opened);
//> {
// name: 'Welcome to your app',
// notification: {
//   text: 'Hello there',
//   isOpen: true
// }}

Microstate transitions always return the whole object. Notice how we invoked the boolean transition app.notification.isOpen, but we didn't get a new Boolean microstate? Instead, we got a completely new App where everything was the same except for that single toggled value.

Transitions for built-in types

The primitive types have predefined transitions:

  • Boolean
    • toggle(): Microstate - return a Microstate with opposite boolean value
  • String
    • concat(str: String): Microstate - return a Microstate with str added to the end of the current value
  • Number
    • increment(step = 1: Number): Microstate - return a Microstate with number increased by step, default is 1.
    • decrement(step = 1: Number): Microstate - return a Microstate with number decreased by step, default is 1.
  • Object
    • assign(object): Microstate - return a Microstate after merging object into current object.
    • put(key: String, value: Any): Microstate - return a Microstate after adding value at given key.
    • delete(key: String): Microstate - return a Microstate after removing property at given key.
  • Array
    • map(fn: (Microstate) => Microstate): Microstate - return a Microstate with mapping function applied to each element in the array. For each element, the mapping function will receive the microstate for that element. Any transitions performed in the mapping function will be included in the final result.
    • push(value: any): Microstate - return a Microstate with value added to the end of the array.
    • pop(): Microstate - return a Microstate with last element removed from the array.
    • shift(): Microstate - return a Microstate with element removed from the array.
    • unshift(value: any): Microstate - return a Microstate with value added to the beginning of the array.
    • filter(fn: state => boolean): Microstate - return a Microstate with filtered array. The predicate function will receive state of each element in the array. If you return a falsy value from the predicate, the item will be excluded from the returned microstate.
    • clear(): Microstate - return a microstate with an empty array.

Many transitions on primitive types are similar to methods on original classes. The biggest difference is that transitions always return Microstates.

Type transitions

Define the transitions for your types using methods. Inside of a transition, you can invoke any transitions you like on sub microstates.

import { create } from 'microstates';

class Person {
  name = String;
  age = Number;

  changeName(name) {
    return this.name.set(name);
  }
}

let homer = create(Person, { name: 'Homer', age: 39 });

let lisa = homer.changeName('Lisa');

Chaining transitions

Transitions can be composed out of any number of subtransitions. This is often referred to as "batch transitions" or "transactions". Let's say that when we authenticate a session, we need to both store the token and indicate that the user is now authenticated. To do this, we can chain transitions. The result of the last operation will become a new microstate.

class Session {
  token = String;
}

class Authentication {
  session = Session;
  isAuthenticated = Boolean;

  authenticate(token) {
    return this.session.token.set(token).isAuthenticated.set(true);
  }
}

class App {
  authentication = Authentication;
}

let app = create(App, { authentication: {} });

let authenticated = app.authentication.authenticate('SECRET');

valueOf(authenticated);
//> { authentication: { session: { token: 'SECRET' }, isAuthenticated: true } }

The initialize transition

Just as every state machine must begin life in its "start state", so too must every microstate begin life in the right state. For those cases where the start state depends on some logic, there is the initialize transition. The initialize transition is just like any other transition, except that it will be automatically called within create on every Microstate that declares one.

You can even use this mechanism to transition the microstate to one with a completely different type and value.

For example:

class Person {
  firstName = String;
  lastName = String;

  initialize({ firstname, lastname } = {}) {
    let initialized = this;

    if (firstname) {
      initialized = initialized.firstName.set(firstname);
    }

    if (lastname) {
      initialized = initialized.lastName.set(lastname);
    }

    return initialized;
  }
}

The set transition

The set transition is the only transition that is available on all types. It can be used to replace the value of the current Microstate with another value.

import { create } from 'microstates';

let number = create(Number, 42).set(43);

number.state;
//> 43

Pro tip: Microstates will never require you to understand Monads in order to use transitions, but if you're interested in learning about the primitives of functional programming that power Microstates, you may want to checkout funcadelic.js.

Transition scope

Microstates are composable, and they work exactly the same no matter what other microstate they're a part of. For this reason, Microstate transitions only have access to their own transitions and the transitions of the microstates they contain. What they do not have is access to their context. This is similar to how components work. The parent component can render children and pass data to them, but the child components do not have direct access to the parent component. The same principle applies in Microstates, so as a result, it benefits from the same advantages of isolation and composability that make components awesome.

State Machines

A state machine is a system that has a predefined set of states. At any given point, the state machine can only be in one of these states. Each state has a predefined set of transitions that can be derived from that state. These constraints are beneficial to application architecture because they provide a way to identify application state and suggest how the application state can change.

From its conception, Microstates was created to be the most convenient way to express state machines. The goal was to design an API that would eliminate the barrier of using state machines and allow for them to be composable. After almost two years of refinement, the result is an API that has evolved significantly beyond what we typically associate with code that expresses state machines.

xstate for example, is a great specimen of the classic state machine API. It's a fantastic library and it addresses the very real need for state machines and statecharts in modern applications. For purposes of contrast, we'll use it to illustrate the API choices that went into Microstates.

Explicit Transitions

Most state machine libraries focus on finding the next state given a configuration. For example, this xstate declaration describes what state id to match when in a specific state.

import { Machine } from 'xstate';

const lightMachine = Machine({
  key: 'light',
  initial: 'green',
  states: {
    green: {
      on: {
        TIMER: 'yellow'
      }
    },
    yellow: {
      on: {
        TIMER: 'red'
      }
    },
    red: {
      on: {
        TIMER: 'green'
      }
    }
  }
});

Microstates does not do any special state resolution. You explicitly declare what happens on a state transition. Here is what a similar state machine looks like in Microstates.

class LightMachine {
  color = String;

  initialize({ color = 'green' } = {}) {
    return this.color.set(color);
  }

  timer() {
    switch (this.color.state) {
      case 'green': return this.color.set('yellow');
      case 'yellow': return this.color.set('red');
      case 'red':
      default:
        return this.color.set('green');
    }
  }
}

With Microstates, you explicitly describe what happens on transition and define the matching mechanism.

Transition methods

transitionTo is often used by state machine libraries to trigger state transition. Here is an example with xstate library,

const nextState = lightMachine.transition('green', 'TIMER').value;

//> 'yellow'

Microstates does not have such a method. Instead, it relies on vanilla JavaScript property lookup. The method invocation is equivalent to calling transitionTo with name of the transition.

import { create } from 'microstates';

let lightMachine = create(LightMachine);

const nextState = lightMachine.timer();

nextState.color.state;
//> 'yellow'

Immutable Object vs Immutable Data Structure

When you create a state machine with xstate, you create an immutable object. When you invoke a transition on an xstate state machine, the value of the object is the ID of the next state. All of the concerns of immutable value change as a result of state change are left for you to handle manually.

Microstates treats value as part of the state machine. It allows you to colocate your state transitions with reducers that change the value of the state.

Framework Integrations

  • React.js
  • Ember.js
  • Create a PR if you created an integration that you'd like to add to this list.
  • Create an issue if you'd like help integrating Microstates with a framework

microstates npm package

The microstates package provides the Microstate class and functions that operate on Microstate objects.

You can import the microstates package using:

npm install microstates

# or

yarn add microstates

Then import the libraries using:

import Microstate, { create, from, map } from 'microstates';

create(Type, value): Microstate

The create function is conceptually similar to Object.create. It creates a Microstate object from a type class and a value. This function is lazy, so it should be safe in most high performant operations even with complex and deeply nested data structures.

import { create } from 'microstates';

create(Number, 42);
//> Microstate

from(any): Microstate

from allows the conversion of any POJO (plain JavaScript object) into a Microstate. Once you've created a Microstate, you can perform operations on all properties of the value.

import { from } from 'microstates';

from('hello world');
//Microstate<String>

from(42).increment();
//> Microstate<Number>

from(true).toggle();
//> Microstate<Boolean>

from([1, 2, 3]);
//> Microstate<Array<Number>>

from({ hello: 'world' });
//> Microstate<Object>

from is lazy, so you can consume any deeply nested POJO and Microstates will allow you to perform transitions with it. The cost of building the objects inside of Microstates is paid whenever you reach for a Microstate inside. For example, let o = from({ a: { b: { c: 42 }}}) doesn't do anything until you start to read the properties with dot notiation like o.entries or with iteration / destructuring.

let [[[[[[c]]]]]] = from({a: { b: {c: 42}}});
valueOf(c.increment());

// { a: { b: { c: 43 }}}

let greeting = from({ hello: ['world']});
let [ world ] = greeting.entries.hello;
valueOf(world.concat('!!!'));
// { hello: [ 'world!!!' ]}

map(microstate, fn): Microstate

The map function invokes the function for each microstate in an array microstate. It is usually used to map over an array of microstates and return an array of components. The mapping function will receive each microstate in the array. You can invoke transitions on each microstate as you would usually.

let numbers = create([Number], [1, 2, 3, 4]);

<ul>
  {map(numbers, number => (
    <li onClick={() => number.increment()}>{number.state}</li>
  ))}
</ul>;

Streaming State

A microstate represents a single immutable value with transitions to derive the next value. Microstates provides a mechanism called Identity that allows to emit stream of Microstates. When you create an Identity from a Microstate, you get an object that has the same shape the original microstate. Every composed microstate becomes an identity and every transition gets wrapped in side effect emitting behaviour specific to the identity's constructor. Identity wrapped Microstate offer the following benefits over raw Microstates.

Structural Sharing

A common performance optimization used by all reactive engines is to prevent re-renders for components whoโ€™s props have not changed. The most efficient way to determine if a value has not changed it to perform an exact equality check, for example: prevValue === currentValue. If the reference is the same, then consider the value unchanged. The Identity makes this possible with Microstates by internally managing how the Identity is constructed as a result of a transition. It will automatically determine which branches of microstates are unchanged and reuse previous identities for those branches.

Memoized Getters

Microstates are immutable which makes it safe for us to memoize computations that are derived off their state. Identies will automatically memoize getters and return previously computed value when the microstate backing the identity has not changed. When a transition is invoked on the identity, the part of the identity tree that are changed will be re-created effectively invalidating the cache for the changed parts of the identity. The getters will be recomputed for state that is changed.

Debounce no-op transitions

Identities automatically prevent unnecessary re-renders by debouncing transitions that do not change the value. This eliminates the need for shouldComponentUpdate hooks for pure components because it is safe to assume that a component that is re-rendering is re-rendering as a result of a transition that changed the value. In other words, if the current state and the state being transitioned to are the same, then a "new state" that would be the same is not emitted.

let id = Store(from({ name: 'Charles' }), next => {
  console.count('changed');
  id = next;
});

id.name.set('Charles');
id.name.set('Charles');

The above transitions would be debounced because they do not change the value. The update callback would not be called, even though set operation is called twice.

Identity Constructors

Microstates comes with two Identity constructors: Store and Observable. Store will send next identity to a callback. Observable will create a stream of identities and send next identity through the stream.

Store(microstate, callback)

Store identity constructor takes two arguments: microstate and a callback. It returns an identity. When a transition is invoked on the identity, the callback will receive the next identity.

import { Store, from, valueOf } from 'microstates';

let initial = create(Number, 42);

let last;

last = Store(initial, next => (last = next));

last.increment();
//> undefined
// callback will be invoked syncronously on transition

// last here will reference the last
last.increment();
//> undefined

valueOf(last);
//> 44

The same mechanism can be used with React or any other reactive environment.

import React from 'react';
import { Store, create } from 'microstates';

class Counter extends React.Component {
  state = {
    last: Store(create(Number, 42), next => this.setState({ last: next }))
  };
  render() {
    let { last } = this.state;
    return (
      <button onClick={() => last.increment()}>Increment {last.state}</button>
    );
  }
}

Observable.from(microstate)

Microstates provides an easy way to convert a Microstate which represents a single value into a Observable stream of values. This is done by passing a Microstate to Observable.from function. This function will return a Observable object with a subscribe method. You can subscribe to the stream by passing an observer to the subscribe function. Once you subscribe, you will synchronously receive a microstate with middleware installed that will cause the result of transitions to be pushed through the stream.

You should be able to use to any implementation of Observables that supports Observer.from using symbol-observable. We'll use RxJS for our example.

import { from as observableFrom } from 'rxjs';
import { create } from 'microstates';

let homer = create(Person, { firstName: 'Homer', lastName: 'Simpson' });

let observable = observableFrom(homer);

let last;
let subscription = observable.subscribe(next => {
  // capture the next microstate coming through the stream
  last = next;
});

last.firstName.set('Homer J');

valueOf(last);
//> { firstName: 'Homer J', lastName: 'Simpson' }

The Vision of Microstates

What if switching frameworks were easy? What if a company could build domain specific code that worked across frameworks? Imagine what it would be like if your tools stayed with you as you progressed in your career as an engineer. This is the world that we hope to create with Microstates.

Shared Solutions

Imagine never having to write another normalized data store again because someone made a normalized data store Microstate that you can compose into your app's Microstate.

In the future (not currently implemented), you will be able to write a normalized data store like this,

import Normalized from 'future-normalized-microstate';

class MyApp {
  store = Normalized.of(Product, User, Category);
}

The knowledge about building normalized data stores is available in libraries like Ember Data, Orbit.js, Apollo and urql, yet many companies end up rolling their own because these tools are coupled to other stacks.

As time and resources permit, we hope to create a solution that will be flexible enough for use in most applications. If you're interested in helping us with this, please reach out.

Added Flexibility

Imagine if your favourite Calendar component came with a Microstate that allowed you to customize the logic of the calendar without touching the rendered output. It might looks something like this,

import Calendar from 'awesome-calendar';
import { filter } from 'microstates';

class MyCalendar extends Calendar.Model {
  // make days as events
  days = Day.of([Event]);

  // component renders days from this property
  get visibleDays() {
    return filter(this.days, day => day.state.status !== 'finished');
  }
}

<Calendar.Component model={MyCalendar} />;

Currently, this is pseudocode, but Microstates was architected to allow for these kinds of solutions.

Framework Agnostic Solutions

Competition moves our industry forward but consensus builds ecosystems.

Unfortunately, when it comes to the M(odel) of the MVC pattern, we are seeing neither competition nor consensus. Every framework has its own model layer that is not compatible with others. This makes it difficult to create truly portable solutions that can be used on all frameworks.

It creates lock-in that is detrimental to the businesses that use these frameworks and to the developers who are forced to make career altering decisions before they fully understand their choices.

We don't expect everyone to agree that Microstates is the right solution, but we would like to start the conversation about what a shared primitive for state management in JavaScript might look like. Microstates is our proposal.

In many ways, Microstates is a beginning. We hope you'll join us for the ride and help us create a future where building stateful applications in JavaScript is much easier than it is today.

FAQ

What if I can't use class syntax?

Classes are functions in JavaScript, so you should be able to use a function to do most of the same things as you would with classes.

class Person {
  name = String;
  age = Number;
}

โ˜๏ธ is equivalent to ๐Ÿ‘‡

function Person() {
  this.name = String;
  this.age = Number;
}

What if I can't use Class Properties?

Babel compiles Class Properties into class constructors. If you can't use Class Properties, then you can try the following.

class Person {
  constructor() {
    this.name = String;
    this.age = Number;
  }
}

class Employee extends Person {
  constructor() {
    super();
    this.boss = Person;
  }
}

Run Tests

$ npm install
$ npm test

microstates's People

Contributors

alexlafroscia avatar courajs avatar cowboyd avatar dependabot[bot] avatar eandy5000 avatar greenkeeper[bot] avatar hakilebara avatar ikr avatar jeffutter avatar keathleydavidj avatar knownasilya avatar leonardodino avatar littlehaker avatar mharris717 avatar micahjon avatar mikelyons avatar minkimcello avatar plynch avatar qpowell avatar ravinggenius avatar rkeeler avatar robdel12 avatar sivakumar-kailasam avatar taras avatar tazsingh avatar teknofiend avatar tommerkle1 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

microstates's Issues

Num State

The core microstate Num wraps JavaScript values of type number. As such, values that are passed into the constructor are automatically coerced into numbers.

new Num("6").valueOf() //=> 6

Numbers can support the standard arithmetic operations:

let num = new Num(1);
num.multiply(2).valueOf() //=> 2
num.divide(2).valueOf() //=> 0.5
num.modulo(1).valueOf() //=> 0
num.pow(3).valueOf() //=> 1

Static properties for a microstate class are not static

If a property is inherent to a microstate class, then all instance ought to be able to share it.

import Microstates, * as MS from 'microstates';


let ms = Microstates(MS.Boolean, true);
console.log("ms.toggle().valueOf() = ", ms.toggle().toggle().toggle().valueOf());

class Confirmation {
  constructor() {
    this.unique = {};
  }
  get isArmed() { return false; }
  get isConfirmed() { return false; }

  arm() {
    return this(Armed);
  }
  reset() {
    return this(Confirmation);
  }
}

class Armed extends Confirmation {
  get isArmed() { return false; }

  confirm() {
    return this(Confirmed);
  }
}

class Confirmed extends Confirmation {
  get isConfirmed() { return true; }
}

let dnd = Microstates(Confirmation);
console.log('initial', dnd.states.isArmed);

let result = dnd
    .arm()
    .confirm();

let result2 = dnd
    .arm()
    .confirm();

console.log('unique 1', result.states.unique);
console.log('unique 2', result2.states.unique);
console.log('result.states.unique === result2.states.unique', result.states.unique === result2.states.unique);

outputs

ms.toggle().valueOf() =  false
initial false
unique 1 {}
unique 2 {}
result.states.unique === result2.states.unique false

Roadmap For Reboot

Order of operations

  • create reboot branch, and reset the test suite to a simple mocha runner. #10
  • Find out a way to run tests in the browser, while still maintaining a simple build process, and sourcemaps. #10
  • Opaque State whose only transition is set() #7
  • Bool state with toggle() #8
  • Num state with standard arithmetic operations #9
  • Record state with an assign and put transitions (basically a microstate boxing JavaScript Object) #11
  • List state with standard list operations.
  • Composable States
    • compose Map
    • compose List
  • Parameterizable States.
  • Multiple Select
  • Overrideable Substates
  • Single Select

Example: Single Selection

Like the Multiselect, Single select is a relatively simple example that folks can grasp almost immediately. It's a little more involved and will require some explanation because of its exclusivity. In other words, any time you select an option in a select, you have to remap all the other options to deselect them.

  • create lo-fi mockups for what the example will look like
  • make sure to highlight the remapping step.
  • illustrate the state machine
  • implement for React
  • add to Gallery if available.

String transitions

We currently only support concat transition on String type.

Vote for which transitions we should support.

default value of a composed simple state

If you define a microstate like

class Products {
  filter = MS.String;
}

It would be helpful to be able to specify a default value for the filter when one is not specified. This not currently possible.

Rename sum to add on Number

Sum is the result of addition of two or more numbers, so to keep it inline with the subtract method, it'd probably be better to rename sum to add on the Number type.

`push` transition does not work in `ArrayType`

It appears that a push for a parameterized array does not work, and the following testcase fails.

describe('pushing onto a parameterized array', function() {
  class Thing {
    description = String;
  }

  it('records both the values and states of the items pushed', function() {
    let array = create([Thing], [])
        .push({description: "weird"})
        .push({description: "behavior"});

    expect(array.valueOf().length).toBe(2);
    expect(array.state.length).toBe(2);
    expect(array.state[0]).toBeInstanceOf(Thing);
    expect(array.state[1]).toBeInstanceOf(Thing);
    expect(array.state[0].description).toEqual("weird");
    expect(array.state[1].description).toEqual("behavior");
  });
});

The valueOf() only contains the last thing pushed on:

 Expected value to equal:
      [{"description": "weird"}, {"description": "behavior"}]
    Received:
      [{"description": "behavior"}]

Furthermore, the state property completely fails insofar as it is equal to []

Transitions are not inherited from superclasses.

Occuring in #33
reproduction:

import Microstates, * as MS from 'microstates';


let ms = Microstates(MS.Boolean, true);
console.log("ms.toggle().valueOf() = ", ms.toggle().toggle().toggle().valueOf());

class Confirmation {
  get isArmed() { return false; }
  get isConfirmed() { return false; }

  arm() {
    return this(Armed);
  }
  reset() {
    return this(Confirmation);
  }
}

class Armed extends Confirmation {
  get isArmed() { return false; }

  confirm() {
    return this(Confirmed);
  }
}

class Confirmed extends Confirmation {
  get isConfirmed() { return true; }
}

let dnd = Microstates(Confirmation);
console.log('initial', dnd.states.isArmed);

let result = dnd
    .arm()
    .confirm();

let result2 = dnd
    .arm()
    .confirm()
    .reset()
    .arm()
    .reset();
console.log("result", result.states);

Expected initial state, actual:

TypeError: dnd.arm(...).confirm(...).reset is not a function

Composable Microstates

Microstates lets you define atoms of data and the state transitions that move you from one valid atom to the next. Hence we have numbers, lists, strings, etc.. However, for more complex interactions, we need to be able to compose these atomic microstates into larger microstates, for example a list of numbers, or a object containing lists of more objects.

We need a way to compose microstates into higher-order objects while still making the API to work with them simple and intuitive. If we lose the intuitiveness of it, then what's the point?

Simple Construction

It must be simple to construct a composed microstate. That means being able to use a pojo to construct it, and to have it "just work". E.g.

let lobs = new ListOfBooleans([true, false, true, true]);

You shouldn't have to have any knowledge about the internal format, but be secure that the individual booleans will be passed to the BooleanState constructor. Among other things, this allows for simple re-hydration of a microstate from persistent data, or something brought in over the wire.

How would we declare this? Probably something like:

import { list, boolean } from 'microstates';

export default list(boolean) {
  transitions: {
    selectAll(current) {
      return current.map(bool => false)
    },
    deselectAll(current) {
      return current.map(bool => true)
    }
  }
}

In this case the list function takes a constructor function and returns a list constructor that passes each of its constitutents to the boolean constructor.

It is possible that we could implement this by having microstate constructors return instances when invoked with new but constructors when invoked without.

In the selectAll and deselectAll transitions, it should also be implicit that the bare false and true values will get passed to the BooleanState constructor.

Recursive transitions

In this example, the booleans should not behave like mere booleans. Instead, we should be able to enact all of the normal boolean transitions, but instead of returning the next boolean state, it returns the next state of the entire datastructure that contains them. E.g.

let lobs = new ListOfBooleans([true, false, false]);
let next = lobs.get(2).replace(true);

which will return a new ListOfBooleans object that contains that boolean value replaced with true instead of false.

Taking Snapshots

Finally, to make things as simple as possible, it should be trivial to get an entire tree of a microstate as a simple object. This will make persistence a snap. The rule to adhere to is that the output of this snapshot should always be valid as the input to the constructor.

my current thinking is that this should be implemented with valueOf() which will recurse through all substates and call valueOf() on them. E.g.

let lobs = new ListOfBooleans([false, true]);
lobs.valueOf() //=> [false, true]

Parameterized arrays should be iterable

When I create a microstate for a parameterized array, I expect the microstate object to look like an array to allow React to iterate the object. Currently, it throws Uncaught Error: Objects are not valid as a React child (found: object with keys.

React supports getting iterators from objects. There are several issues on the subject. We should consider making microstates iterable by default.

Example https://runkit.com/taras/parameterized-arrays-should-be-arrays

Links

What is the value?

This issue is the continuation of the conversation from #e-microstates channel.

Here is the transcript of the conversation:

taras: One thing that I'm not clear about is what happens with the value when it's boxed in. Is a microstate permanently converting a value it receives or is it type casting it internally and exporting the original value?

cowboyd: it โ€œcapturesโ€ the value, but it does massage it somehow, but only when necessary

let object = {};
let state = new State(object);
state.valueOf() === object //=> true

However, this will not be the case if a merge happens, but maybe it should.

const Thing = State.extend({
  one: 'two'
});
let object = {};
let thing = new Thing(object);
thing.valueOf() //=> {one: 'two'};
thing.valueOf() === object // false

But now that I think of it, maybe value of should be {} because it does not represent any change

The other case is what do you do when you have a nested state:

let object = {};
const Thing = State.extend({
  one: new State(1),
});

let thing = new Thing(object);
thing.valueOf() //=> {one: 1}

but again, maybe thatโ€™s not right. Maybe thing.valueOf() should be object, because there has been no change.

It might be worth having a materialize function that collects the values of all properties on the whole tree.

Action Plan for 0.1.0

TODO

  • curry context into transitions (ie. first argument should be current value) #1
  • partition transitions from other methods on MicroState #1
  • eliminate replace operation for primitive micro states (replace with merge) #1
  • make replace a transition #1
  • add ability to run tests in the browser https://github.com/taras/microstates.js/pull/1
  • tests now run in node too
  • Safari doesn't like defineProperty(Class, 'name', {value: name });
  • Tests don't run in Phantom.js
  • TodoItem MicroState
  • Complete TodoItem creates new TodoList MicroState
  • TodoItem must also operate in isolation

Proposal: add TodoMVC implementation examples with React, Glimmer, Vue and Angular

I showed our current readme to a friend and his feedback was that some code examples with DOM would be helpful. I'd like to show how microstates can be implemented for different frameworks. I already created a TodoMVC microstate.

What do you think about creating an examples directory that has example of apps in React, Glimmer, Vue and Angular using the same TodoMVC microstate.

Here is the microstate definition that I already have,

import microstate, * as MS from 'microstates'

/**
 * TodoMVC is a Microstate type.
 *
 * A microstate is a value and type description. The type description is used to interpret the value
 * and generate state and transitions for the value. The state will be bound to the template and rendered.
 * Transitions will be used to create actions that will transition the state and update the DOM tree.
 *
 * Types describe:
 * 1. The structure of the state that the type represents.
 * 2. The custom transitions that can be performed on this type.
 * 3. The computed properties that should be applied to the state object that's derived from this type.
 */
class TodoMVC {
  /**
   * COMPOSED TYPES
   *
   * Composed types describe the structure of the microstate and how the composed value can be transitioned.
   * In this example, we're only composing an array into this microstate, but we can compose microstates of any
   * depth. Microstates will know how to transition that structure.
   */
  todos = MS.Array

  /**
   * COMPUTED PROPERTIES (aka getters)
   *
   * Computed properties are applied to the state that's generated from a type. The context of the computed
   * property is the state object. This gives developer access to composed states on this microstate. It can
   * be used to derive values.
   *
   * In Redux world, this would be similar to Reselect except Microstates doesn't currently support memoization.
   * We're planning to apply several levels of memoization in the future.
   */
  get completedCount() {
    // in computed properties, `this` references state object which has composed state values on it.
    return this.todos.filter(({ completed }) => completed).length
  }

  get nextId() {
    return this.todos.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1
  }

  /**
   * CUSTOM TRANSITIONS
   *
   * Custom transitions allow to define operations that are specific to the domain of the data that the microstate represents.
   * Transitions are similar to Redux reducers in that they are pure functions that receive state and return new state.
   *
   * When a transition is invoked, the transition function has access to the following:
   * 1. current state of the the microstate via the first argument - unlike Redux, this state is already initialized, so you do not need to define initial value.
   * 2. arguments passed to the transition as the rest of the argument after the first argument
   * 3. context object is a microstate constructor for performing batched transitions on the microstate's state
   */

  completeTodo(current, todo) {
    /**
     * microstates doesn't support transitioning state that is composed into an array (yet).
     * We can transition the array, but not values in that array. We're planning
     * to add this feature in the future but we need to figure out how to do that properly.
     *
     * Find the todo that we want to update and replace it with new item that has completed true.
     */
    return this().todos.replace(todo, {
      ...todo,
      completed: true,
    })
  }

  editTodo(current, todo, text) {
    // Find the todo that we want to update and replace it with new item with changed text.
    return this().todos.replace(todo, {
      text,
      ...todo,
    })
  }

  deleteTodo(current, todo) {
    // Filter here is a transition on todos array.
    return this().todos.filter(item => item !== todo)
  }

  addTodo(current, text) {
    // Push is transition on todos array.
    return this().todos.push({
      text,
      id: current.nextId,
      completed: false,
    })
  }

  clearCompleted(current) {
    return this().todos.filter(({ completed }) => !completed)
  }
}

Document merge transition

We're missing documentation about merge transition which is on composed types. set replaces the value of the composed type, where merge combines the value using Ramda's mergeDeepRight.

Reset project

  • Create reboot branch
  • Remove Broccoli
  • Remove testem
  • Use mocha directly
  • Test in the browser
  • Check if sourcemaps are working

Example: MultiSelect

Multiselection is a simple, familiar, and tractable example of something that is state-driven, but that might not be understood to be as such.

  • create lo-fi mockups for what the example will look like
  • illustrate the state machine
  • implement for React
  • add to Gallery if available.

Build microstates.io website

We have an (interim) logo, we have desire to market and bring microstates to the world... It naturally follows that we should have a website.

It should help you get started if you're new to microstates, or also provide api docs.

Exploring Internal API improvements to Microstates

We should be able to create an ObservableMicrostate from a Microstate.

For example,

let ms = microstates(State);
let { transitions, subscribe ) = new MicrostateObservable(ms);

Instead, we're passing State into MicrostateObservable constructor. The constructor is repeating code from microstates() because it's difficult to wrap every transition in a function that would allow to next the value returned by the transition. In MicrostateObservable, mapTransitions(tee, callback) receives a callback that next the value. It's also important to point out that the transitions tree, unlike state tree, is stable and doesn't change.

There might be another way to think about the relationship between state and transitions. We could think about transitions as a stream of transitions.

For example, we could do something like this,

let { state, transitions } = microstates(State);

Observable.from(transitions).subscribe({
  next([ transition, ...args ]) {
    let newState = transition(...args);
    // at this point, we would dispatch an action in Redux or set state without Redux.
    // we would even push it into a stream of states if we have one
    // this will also make it possible to dispatch an action with the new state
  }
})

We still need a better way to map transitions to make it easier to wrap them with a callback. I'm open to suggestions on how we might do this.

Base State

At its core, a Microstates encapsulate a simple JavaScript value and describes the valid state transitions from one value to the next. The simplest State then, is a completely opaque container that can hold any JavaScript value, boolean, number object, function or otherwise. The only transition that it has is the set() which does nothing but returns a new microstate representing the new value..

let state = new State(5);

The valueOf() function "unboxes" the value to get at the raw reference:

state.valueOf() //=> 5

The only transition available to an opaque state is the set transition. It returns a completely new microstate.

let next = state.set("Hello World");
next === state //=>  false
next.valueOf() //=> "Hello World"

Taken on its own, this is not particularly useful, however, this base state can be extended to add new properties and new transitions.

const MyNum = State.extend({
  get double() {
    return this * 2;
  },
  get negative() {
    return -this;
  }
});

let num = new MyNum(5);
num.double //=> 10
num.negative //=> -5

Note that the getters in this microstate have the this value bound to the value of the microstate when called. These properties should, and therefore must depend only on the value of the microstate and nothing else.

Transitions

In order to derive new microstates from the existing one, microstates support the concept of transitions. These are pure functions contained with in the microstate that return a new microstate representing the transformation in state. For example, to implement a multiplication transition:

const Arithmetic = State.extend({
  transitions: {
    add(value, amount) {
      return value + amount;
    },
    subtract(value, amount) {
      return this.add(-amount);
    }
  }
});

let arithmetic = new Arithmetic(5);
arithmetic.add(5).valueOf() //=> 10
arithmetic.subtract(3).valueOf() //=> 2

There are several things to note.

  1. The current value of the microstate is always passed as the first argument to the transition.
  2. Inside a transition function, the value of this is bound to the microstate itself. This allows you to define transitions in terms of each other as in the case of subtract being defined in terms of add.

Immutability

Microstates are immutable, and are frozen upon construction.

let state = new State(null);
state.foo = 'bar';
state.foo //=> undefined

Create pull request template

If we want to foster contributions from the community, then we need a way to guide it towards the highest quality level of pull requests possible.

Let's create a standard PR template for use across all of our repos

Performance issue

Hi, guys tried to run some perf benchmarks with your lib and results look weird https://github.com/zhDmitry/statem-perf . Can u clarify why the update results look so bad ?

           6 op/s ยป update //update in one method but with multiple set
           4 op/s ยป update 2 // call subentity update
           9 op/s ยป update 3 //  make set on all entity and updating using reducer
       4,560 op/s ยป update 4 // noop update

desugar the composition DSL

Currently, we provide a mechanism that allows to express Type structure via class properties or fields syntax. For example,

class Person {
  name = String;
  age = Number;
  pastResidences = [Address]
}

When we create a microstate with Microstate.create(Person), we built a tree from this Type. In the process, we call desugar to convert the provided Type into a Type that microstates can use to build state and transitions.

After the tree is created, we call flatMap on the tree and lazily invoke initialize transition to allow each type that describes an initialize transition an opportunity to convert the microstate's value into value that it needs to setup that microstate.

The construction of the tree is currently someone implicit. We could make it more explicit by treating our composed Type declaration as sugar for longer syntax. The above example would be equivalent to,

class Person {
  name = Microstate.create(String)
  age = Microstate.create(Number)
  pastResidences = Microstate.create([Address])
}

When microstates creates a Tree, it would convert the sugar Type declaration into the long form Microstate declaration. We would use this tree from this created Microstate when building the initial tree.

This could make it easy for us to specify default values for #50 by doing this,

class Person {
  name = Microstate.create(String, 'John Doe')
  age = Microstate.create(Number, 18)
  pastResidences = Microstate.create([Address])
}

We could further extend the sugar syntax by allowing to express default value with new Type(value) syntax. For example,

class Person {
  name = new String('John Doe') // desugar to Microstate.create(String, 'John Doe')
  age = new Number(18); // desugar to Microstate.create(Number, 18)
  pastResidences = [Address]
}

We can make it easier to consume Microstates in TypeScript environment using this long from syntax.

Example: History

History is the closest thing to a holy grail in the UI world. Even systems like Redux don't truly implement history, they just let you see a timeline of state snapshots.

A history microstate on the other hand will make adding undo/redo to any application a piece of cake. It should support not just undo/redo, but also branching.

Everything is on display with history: parameterization, overriding transitions, etc...

  • create lo-fi mockups for what the example will look like
  • illustrate the state machine
  • implement for React
  • add to Gallery if available.

Record State

The Record encapsulates a non-primitive JavaScript object and contains transitions for mapping string keys to any javascript value. Just like you would with a javascript literal:

let record = new Record({
  hello: 'World',
  goodbye: 'Bad Times'
});

record.hello //=> 'World`;
record.goodbye //=> `Bad Times`
Object.keys(record) //=> ['hello', 'goodbye']

In order to change the value at a particular key, use the put transition:

let next = record.put('hello', 'Planet');
next.hello //=> 'Planet'
next.goodbye //=> 'Bad Times'

To remove a property from a Record, use the delete transition. This property will now be undefined, and will no longer be enumerable.

let next = record.delete('hello');
next.hello //=> undefined
Object.keys(next) //=> ['goodbye']

In order to update many properties at the same time, use the assign transition. This will return a new microstate with the specified properties merged into the previous state.

let next = record.assign({hello: 'Friend-o', name: 'Bob'});
next.hello //=> 'Friend-o'
next.goodbye //=> 'Bad Times'
next.name /=/> 'Bob'

Any properties that are not assigned explicitly are left intact.

Optional - requires feedback.

When extending Record (which is what you want to do most of the time), transitions will, by default merge in the value returned by the transition function as opposed to replace the value entirely. This is because most of the time you want to just set a single value, and it would be unecessary to require you setting the whole object every time.

For example, suppose we had a class a-la google maps to define latitude and longitude.

const LatLong = Record.extend({
  setLat(current, lat) {
    return { lat };
  },
  setLong(current, lng) {
    return { lng };
  }
});

If we did not support merge, we would have to have a lot of boilerplate in each transition to copy over existing values:

const LatLong = Record.extend({
  setLat(current, lat) {
    return { lat, lng: current.lng };
  },
  setLong(current, lng) {
    return { lat: current.lat, lng };
  }
});

Or do we say it is enough to explicitly 'put' or 'assign' since we have those transitions anyway?

const LatLong = Record.extend({
  setLat(current, lat) {
    return this.put('lat', lat);
  },
  setLong(current, lng) {
    return this.put('lng', lng);
  }
});

Support for getters

I tried creating a getter on a Microstate class and I found that they only work on the hash that's passed to extend. That value is cached and not re-computed for new microstate instances. I would expect getters to be transferred to each new instance.

I created a branch with some failing tests.

I'd like to implement this change. I want to make sure that I'm heading in the right direction. Is the problem that value is cached here?

Supporting real classes

From our spike on microstates/ember#69

Basically, you have to include IE11 as a target for microstates to work, because the way classes are used doesn't work with Babel (Using a class without the new keyword). Many people are building apps that don't include IE11, so this is a big issue for performance.

Example: FileUpload

Microstates is currently hungry for examples. We're trying to bootstrap a community and it's difficult to explain without a picture to point to.

File Upload is a great example of a state machine that is well understood, we actually have diagrams for, and folks in the community could immediately begin to slobber over.

  • create lo-fi mockups for what the example will look like
  • illustrate the state machine
  • implement for React
  • add to Gallery if available.

Make state stable

Stability means that for any given type and value, microstates should always return the same state.

This is helpful when used in frameworks like React and Ember that use exact equality check on props to determine if a component needs to be re-rendered.

There are two types of state that need to be stable: composed instances and getters. Getters are easier of the two because getters are functions and our instances are immutable. For every instance, a getter can only ever have one value. We can stabilize getters by caching the return value of each getter for any given instance.

Stabilizing instance creation means to return the same state instance for a set of parameters. For example, creating a microstate from type Person should return the same instance when given the same properties, regardless of how many times it is called create(Person, { name: 'Taras' }).state === create(Person, { name: 'Taras' }).state

In early conversations with @cowboyd on this subject, Charles suggested that we could make state instances stable by introducing referential transparency into funcadelic.

With referential transparency in microstates, the instances would be cached based on Type and reference to the value rather than the value itself. As result, Microstaes would return different instances for a Type if a reference to a value is different even if the value is the same. For example, create(Person, { name: 'Taras' }).state !== create(Person, { name: 'Taras' }).state because { name: 'Taras' } !== { name: 'Taras' }.

I would like to propose that we consider caching instances based on shallow compare of params as React & Ember optimize component re-rendering against props. Since Microstates are hierarchical, we can do a shallow equality compare while collapsing the state tree and return a previous instance if one is available.

Add replace array transition

It's often helpful to be able to replace an element in an array.

let array = Microstates.from(Array, [ 'a', 'b', 'c']);

array.transitions.replace('a', 'd');
// => [ d, b, c ]

Example: Polygon

Could be used to render various shapes to a canvas. Might be fun to explore:

  • rotation

  • origin

  • sides

  • create lo-fi mockups for what the example will look like

  • illustrate the state machine

  • implement for React

  • add to Gallery if available.

Introduce `Any` type

Any type represents any potential type. It will only have set transition which will allow it to be changed to any other type.

class Any {
  static create(value) {
    if (value) {
      return Microstates.from(value);
    }
  }
}

^^ We could even have a default static create method that might look like this.

Microstates.from(undefined) should be same as create(Any);

README.md does not explain why.

Right now, we have a lot of what in the readme, but not a lot of why. The first 5 sentences are our chance to set the hook, It needs a introduction that explains

  • there is a problem with current state management solutions
  • what is unique about microstates that you won't find anywhere else.
  • links to non-trivial examples + further reading.

Number Transitions

We currently only support push transition on Number type.

Which operations should we support?

Example: navigation tree.

Navigation trees are fun little pieces of UI. They are less used in web applications than they are in native apps, but they still pop up from time to time.

What's important to demonstrate to microstate users though is that microstates can be recursively defined. In other words:

class Tree {
  content = Any;
  children = {Tree};
}

Is a perfectly reasonable definition for a microstate class.

  • create lo-fi mockups for what the example will look like
  • illustrate the state machine
  • implement for React
  • add to Gallery if available.

Simple way to pass microstate values downstream

Many microstates carry information forward as they progress through their state diagram. For example, consider the state machine for a validation Rule. As it stands, we have to manually unbox the value of the current microstate to pass it down to the existing microstate.

import { create } from 'microstates';

class Rule {
  input = String;
  description = String;

  get isIdle() { return true; }
  get isTriggered() { return false; }
  get isRunning() { return false; }
  get isFulfilled() { return false; }
  get isRejected() { return false; }

  setInput(input) {
    return create(Triggered, this.valueOf())
      .input.set(input);
  }
  reset() {
    return create(Rule, this.valueOf())
      .input.set('')
  }
}

class Triggered extends Rule {
  get isTriggered() { return true; }

  run() {
    return create(Running, this.valueOf());
  }
}

class Running {
  get isRunning() { return true; }

  fulfill() {
    return create(Fulfilled, this.valueOf());
  }

  reject(reason = "") {
    return create(Rejected, this.valueOf())
      .reason.set(reason);
  }
}

class Fulfilled extends Rule {
  get isFulfilled() { return true; }
}

class Rejected extends Rule {
  reason = String;
  get isRejected() { return true; }
}

By passing the valueOf() for the current microstate, the input property gets passed down the line to each subsequent state.

It would be nice if we automatically unboxed the value so that we could write (among other things) the fulfill method like so:

  fulfill() {
    return create(Fulfilled, this);
  }

Interactive Gallery

A picture is worth a thousand words, and an interactive example is worth a thousand pictures.

It would supercharge learning if we had a list of examples (beyond just the TodoMVC) that you could play around with and also see the code examples in your favorite framework.

For example, on the side bar would be navigation for the example

  • select box
  • history
  • file upload

And then when you focused on one of those, the detail record would show

  • the state machine diagram described by the microstate.
  • the code corresponding to that state machine.
  • a list of tabs with live demos of it working in various frameworks
  • there might be quirks or intricacies associated with each individual framework, so there should be room for notes there.

My thinking is that this is currently what is now the 'playground', but should be more aptly named the "gallery"

Boolean State

The microstate primitive Bool wraps a simple boolean value.

No matter what you pass to the constructor, it is naturally coerced into a boolean value:

new Bool(true).valueOf() //=> true
new Bool(false).valueOf() //=> false
new Bool("Ohai") //=> true
new Bool(null) //=> false

The same applies if you set a boolean value to explicit value:

new Bool(false).set("Ohai").valueOf() //=> true
new Bool(true).set(null).valueOf() //=> false

The only other transition available on a boolean value is toggle:

new Bool(true).toggle().valueOf() //=> false
new Bool(false).toggle().valueOf() //=> true

Callback after Transition

When we start integrating microstates into Ember helpers, we'll need the helper to know when a transition occurred so it'll trigger recompute on the helper. We need the microstate to notify us of a transition.

What if a microstate constructor received a second argument which would accept a callback? After the transition, we'll call the callback and pass new microstate as an argument.

I created a gist to show what different ember-microstate helpers might look like with this pattern. https://gist.github.com/taras/48839269bcda7ffc44d098c1b5f1bee2

Thoughts?

Create issues template

What is it that we need to make an issue great? I don't know, but non-helpful issues are really annoying.

If possible, would like to have an issue template in place for all of our repositories.

Allow microstates for type-shifting and chaining inside parameterized types.

Let's say I have a Form object with a list of Field states. Every field has its initial input, and a way to revert its input back to the initial value.

class Field {
  initialValue = String;
  input = String;
  reset() {
    return this.input.set(this.state.initialValue);
  }
}
class Form {
  name = String;
  fields = [ Field ]
}

Then, suppose I want to have a reset transition at the form level that resets all of the fields back to their initial value:

class Form {
  //....
  reset() {
    return this.fields.map(field => field.reset());
  }
}

This is currently impossible because the object passed to the map function is not a Field microstate, it is the valueOf() the Field microstate.

We need a way to access the transitions of the parameterized type, so that they can compose just as easily as a normal transition.... This includes typeshifting.

This testcase fails:

describe('Mapping parameterized Arrays', function() {
  class Thing {
    description = String;
  }

  class Bling {
    description = String;
  }

  it('loses type in a map', function() {
    let array = create([Thing], [{description: "fun"}])
        .map(item => create(Bling, item));
    expect(array.state.length).toBe(1);
    expect(array.state[0]).toBeInstanceOf(Bling);
  });
});

This might be related to #75

Provide an API to allow extending built in Types

Our types should do a little as necessary but allow people to extend them if necessary. The changes that they introduce should be local to their implementation. One of the possibilities is to allow people to extend the built in Microstates class and override built in types.

For example,

class MyString extends StringType { 
}

class MyMicrostates extends Microstates {
   types = {
     String: MyString
   }
}

The above example would need to work with static create method.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.