Composable state primitives for JavaScript
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.
With Microstates added to your project, you get:
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.
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.
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:
…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.
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,
…You can use the object dot notation to access sub microstates. Using the same example from above:
theHomerCar.designer;
//> Microstate{ name: Microstate'Homer', age: Microstate39 }
theHomerCar.designer.age.state;
//> 39
theHomerCar.name.state;
//> The HomerYou 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' }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{ id: 1, title: 'Hello World' }
//> Microstate{ 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{ id: 1, title: 'Hello World' }
//> Microstate{ id: 2, title: 'Most fascinating blog in the world' }
//> Microstate{ 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!
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{ id: 1, title: 'Hello World' }
blog.posts.entries['2'];
//> Microstate{ 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{ id: 3, title: 'It is only getter better' }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.
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;
//> truePro 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
valueOf(opened);
//> {
// name: 'Welcome to your app',
// notification: {
// text: 'HelNo open issues yet, or sync has not completed.