Fun State Management With Ember and Microstates -Part 2: Complex Types

Brandyn Bennett
7 min readOct 11, 2019

--

Photo by Erda Estremera on Unsplash

In my last article I explained my team’s motivation for using Microstates.js in our Ember app and what the payoffs have been. In this article I’m going to dive deeper into how Microstates work. I’m also going to provide some useful tips for working with complex Microstates.

What Even is a Microstate?

In order to understand some of the nuances of working with complex types, I first need to explain what Microstates are and how they work at a high level.

My interpretation of the word “microstate” is most easily understood when contrasting it against the word “macrostate”. I tend to think of a “macrostate” as the kind of state you would persist to the database. This is the kind of thing that Ember Data handles really well and is often centered around the domain of your app.

Contrast that with “microstates” which I interpret to be the kinds of state that are not persisted to the database. Instead, this kind of state is most often seen in pure UX logic: “is the modal open or closed”, “which column and direction is the table sorted”, “is the button green or blue”, etc. In Ember we typically store this kind of state inside a component or controller. However, as I showed in my last article, this can become unwieldy when dealing with large blocks of state or deeply nested component hierarchies.

A More Technical Definition

In terms of the Microstates.js library, a “microstate” is a protective box around your state data. When you create a new Microstate object you often give it an initial value.

“Homer” String going into Microstate Box

The Microstate protects your state data from being mutated directly. It does so by only allowing “writes” via the Microstate’s transition methods. The transition methods guarantee a brand new object is created instead of the new one being mutated.

“Homer” Microstate becoming “Homer Simpson” Microstate

Because the Microstate objects protect your state in this way it affects the way you access the state when you need to “read” it. For simple types (Boolean, String, Number) you have to use .state to access the data.

Sting coming out of Microstate box with `.state`

For complex types (Object, Array, Custom Types), you need to use the valueOf function to get the state out.

homer object being access with `valueOf`

The requirement to use valueOf and .state to access data on a Microstate is different than we’re used to in Ember, but it is a worthwhile tradeoff.

Now that we understand better what a Microstate is, let’s look at some special considerations for complex types.

Array Microstates

The first thing to know about Array Microstates is that they are not Arrays. They would be better described as “Array-like Objects”. Microstate Arrays leverage Symbol.iterator to make them iterable. This gives them some of the same abilities as an Array, but not all of them.

Things they can do:

  • Be looped over with for or {{#each}}
  • Spreading [...myArrayMicrostate]
  • Use the Microstate version of some Array methods

Things they can’t do:

  • Access by index myArrayMicrostate[2]
  • Use normal Array methods

How Do I Get at Specific Index?

Likely the most noticeable and frustrating thing on the can’t list is the inability to access Array Microstate values at a specific index. Fortunately, the workaround is pretty simple: [...myArrayMicrostate][2].

Array Microstates can use the spread operator, which means we can copy the contents of the Array Microstate into a regular Array and then use the index.

What About My Array Methods?

The Array Microstate object gives us some Array methods we’re used to such as push and pop, but they do not behave like the native Array equivalents. In Microstate version all the methods transition your Microstate to a completely new object, whereas some of the native Array methods mutate the array in place.

Some methods like reduce and the Ember ArrayProxy methods like findBy are completely absent a Microstate equivalent. In these cases you’ll need to follow the same pattern we used for index access and copy the Microstate Array with a spread [...myArrayMicrostate].reduce(). The same rule applies if you want to use the native method version instead of the Microstate one. For example [...myArrayMicrostate].map() would return you a new native Array instead of transitioning the microstate to new state.

Array of Microstates vs Microstate Array

Both of these data structures can be used, but you should be careful about choosing the right one for the right job. To avoid confusion and keep consistency I try and stay with the Microstate Array as much as possible create([Person]). However, I sometimes will work with a native Array of Microstates [create(Person), create(Person)] if I need to access native Array methods like I explained above. After I’ve done my work with the native Array methods I often will convert the result back into a Microstate Array create([Person], normalArray.map(myMicrostate => valueOf(myMicrostate)).

Object Microstates

Object Microstates are similar to native JS Objects in lots of ways with a few subtle differences:

  • Change values with myObjectMicrostate.put()
  • Delete keys with myObjectMicrostate.delete()
  • All object data lives inside entries. myObjectMicrostate.entries.foo
  • All keys are on the keys property. myObjectMicrostate.keys
  • All values are on the values property. myObjectMicrostate.values

Now let’s explore some considerations that are common to all complex Microstate types.

Equality

When you create a Microstate with a complex type it is stored in the Microstate by reference, not by value.

Example of coffee cups being copied by reference vs value from https://www.mathwarehouse.com/programming/passing-by-value-vs-by-reference-visual-explanation.php

This means that if you create two Microstates with the same Object, the values will be equal even though the Microstates themselves aren’t.

Transitions

The Microstate will reference the original object UNTIL you call a transition on it.

When you call a transition method, the new Microstate that’s created will copy the entire object and not mutate the original.

The value will be copied to a new object no matter how deep in the tree you modify something.

Using valueOf

Don’t Forget to import

There is a global valueOf function that will get used if you don’t import the function from @microstates/ember. If you use the global one your linter will likely not catch it as an undefined variable. You will also see unexpected errors when you try to use it like:Cannot convert undefined or null to object

value-of Template Helper

There is a {{value-of}} helper that you can use in your templates when you need to pass a POJO into a component instead of a Microstate object.

Making Modifications Directly to the Value

When you want to use an Array method like replace, or reduce, on an Array Microstate it can be very tempting to use valueOf(myMicrostateArray) and perform your method on the returned Array. BE CAREFUL. If you do this you lose all guarantees of immutability and introduce the possibility of modifying data on other variables pointing to your object.This is especially unsafe when using a “mutate in place” method like pop or push. Instead I recommend sticking to spreading the Microstate Array into a new native Array and doing your modification that way. valueOf was meant for reading, not writing.

Complex Types and the Store

In my last article I explained the basics of the Microstate Store function. One nuance to be aware of when dealing with a Microstate that is wrapped in a Store is that the return value after a transition may not be the full Microstate tree. A Microstate wrapped in a Store will return the deepest property that performed the transition.

This nuance makes it possible to not expose the entire state object with every component that uses it. Components are only aware of the pieces of state that are relevant to them.

Default Values

When you create a custom Microstate type you use class field notation on the attributes to specify what type each attribute should have.

One of beautiful things about the class field spec is that it gives JS classes a way to specify default values. So how do we do that with Microstates when we’re already using the class field value to specify the attribute’s type? You can specify a default value by creating a Microstate with the type and value you want as the default.

Default for the Microstate not the Value

One thing to be aware of when working with default values is that they are only applied on the Microstate object and not the underlying value.

In this example we instantiate a Person Microstate with an empty object. That means that when we access the valueOf the Microstate we will get back the empty object without the default values. This is because Microstates are a wrapper around the value we give and don’t manipulate them until we explicitly tell them to do a transition.

However, like we can see in the above example, even after a transition only the affected properties will be part of the “value”. The properties not transitioned will still not have default values on the underlying “value”.

Conclusion

Microstates give us a powerful way to work with predictable, immutable state with a concise and simple syntax. There are some subtleties you’ll encounter when working with complex types, but now that you are aware of them you are even more empowered to manage your UI state.

What’s Next?

In my next article I’m going to give some cookbook-like patterns you can do with Microstates that will help make your life easier.

--

--