Solid State

So enforced unidirectional flow in a reactive library. This is a challenge in its own right since these are libraries built on mutation. We can’t just use typical immutable patterns. Typically when some nested data changes you would shallow clone along the path of the change while keeping references to unchanged branches. For example, picture changing a users street number from 101 to 102:

const state = {

...prevState,

user: {

...prevState.user,

address: {

...prevState.user.address

streetNumber: 102

}

}

}

Cloning the tree from its root would lose all reactive references. There are other reactive nodes subscribed to points on this tree so we cannot throw it out. At this point, after appreciating the benefits of immutability you might be wondering why even cling on to the hope of using a reactive library. But again there is a simple solution.

If you look at that immutable change example you can see that there is a lot left to be desired. It can be tedious to do something that with a mutation would have simply just been:

state.user.address.streetNumber = 102;

Luckily, libraries have been trying to find ways to make using immutable data structures easier for years. Mostly they use methods to turn the trees back into paths. In basic they attempt to give a mutable API to immutable data structures. Yet because of immutable data’s need to control from the root, they have to come up with some interesting API’s and it is these API’s that we can mimic since they look mutable but still offer that level of control.

So the key to Solid’s state object is it is an ES2015 Proxy object. Not that different than a MobX Observable. It tracks accesses and wraps children in their own proxies. So we can track all the reactive atoms in the tree. However, for Solid, I have deliberately made the Proxy read-only. When you create State in Solid it is split into that Proxy and a setter:

const [state, setState] = createState({/* some data */});

No matter what anyone does to that state alone, it is not changing. You can pass it freely to descendants with fear of it changing. Only the setState method has the ability to change it. You can create specialized update methods wrapping it and give precisely which Components you choose the ability to update what state matters to them. It’s the global state pattern but localized.

So how does setState work? I took my favourite immutable libraries and implemented both an immutable paradigm and a mutable one. The immutable one is terse and resembles React and ImmutableJS setIn :

//top level

setState({ count: state.count + 1 }); //function setter on path returning new state

setState("count", c => c + 1): //nested path

setState("user", "address", "streetNumber", 102);

The mutable one takes after Immer. The state provided from the function is wrapped in a different proxy that allows mutations. But you are only able to mutate that state or any descendant of it in the context of the function.

setState(produce(s => { s.user.address.streetNumber = 102; }))

The real key though is that both these methods use Solid’s built-in batching on top of a mutable system. Which means the performance is significantly better than any Immutable system can be. There is no cloning, no actual immutability under the hood. You get all the benefits of Immutability, with an API that you were willing to use to be Immutable, with none of the performance downsides.