Primitives in Solid

So working backwards, understanding how we wish to create our applications should inform the design of primitives. Fine-grained reactive libraries all consist of 2 base primitives: a reactive atom often referred to as an observable or signal, and a tracking context called a computed or computation. From there it is common to split computations that derive values from the more general ones that produce side effects. In Solid, those are createMemo and createEffect respectively. Additionally, in modern libraries, we often see the use of proxies to create deeply reactive trees as the effort of deeply wrapping and mapping nested structures is a considerable manual effort.

I’ve used many reactive libraries through good and bad times, learning a lot of lessons along the way while using them myself and teaching other developers. No guided practices for composition was one of the problems that plagued the early days. View models while very similar to components were an overloaded construct as they applied to just about everything. As described in the previous section we now have a more structured approach through classification. Reactive primitives have similar issues.

1. Implicit Dependencies

First of all, automatic dependency detection for all the good it does can be difficult to maintain if there are side effects. Sort of obvious statement in hindsight, but early libraries didn’t point out a difference or that you should be aware of it. I actually wrapped Knockout early with a method to enforce explicit dependencies. I told my developers they had to use it if their computation didn’t return a value and that if it did it should not write to any other observable. Why not make pure computations state their dependencies? Mostly that they never seemed to cause issues. You’d generally only be track data you needed to produce that one specific value. These never produced infinite loops.

As many of you know, while Solid has a primitive createDependentEffect that requires explicit dependencies, that isn’t the default. I did debate this, but view bindings need automatic tracking. More than anything I’ve found the need for createEffect reduced significantly in recent years. Almost all of our old effectful computations were based on data loading and callbacks with unpredictable resolution timing which could lead to unexpected dependencies. Promises and primitives to handle those have made this much smoother. Another common case was updating data in deeply nested structures. Proxies can easily handle this now. The remaining cases are mostly to synchronize with a store or some external system. Exactly what it is intended to do. Still, every well designed reactive library should have a way to set explicit dependencies.

2. Hidden Reactions

Unlike issues around implicit dependencies that have lessened over time, this has only gotten worse. As we add more Getters/Setters and Proxies we stop realizing we are dealing with reactive data. It’s natural to think the getters are the bigger problem since you cannot easily tell what’s being tracked. However, we are likely only accessing the values that we need to derive the value or produce an effect. If one of those changes it should trigger. The majority of the cognitive overhead is reduced by the getters since accessing them as values to pass to functions or derive new values has no noise. If you treat data as immutable in these derivations you will never have any issues. Tracking a dependency does not equate to doing a bunch more work.

Triggering one does. We should be very aware when we are updating reactive data because that is when the work happens. Blaming dependency tracking is like complaining that your function requires parameters. Calling the function is the real cost. It should be very obvious then why two very predominant patterns in reactive libraries should be considered anti-patterns, 2-way binding and proxy/object setters.

// Does this do anything other than set a value?

data.name = "John"; // Why does my library require me to assign an array again?

array.push({ name: "John" });

array = array;

This is code you might find with some reactive libraries. Do you know what this is doing? Sure calling an explicit setter isn’t telling all that is happening but at least I know it is. I didn’t accidentally set a value and watch my view implode.

3. Lack of emphasis on directionality

Know what the best part of explicit setters? Read/write segregation. Why is this such a big deal? While fully serviceable by an Object-Oriented model since we are dealing with many data points in each layer with related behaviours — I mean it feels like a Class to me — grouping this together doesn’t afford the appropriate level of control.

Look at what we are dealing with from the previous section. Our view is just a composition of data transformations facilitated by functions to organize our mental model. The result isn’t that different than Functional Reactive Programming. Sort of like RxJS streams, just that you build it in layers. Instead of focusing on the full transformation of a single stream through time which lends to courser granularity in your reactivity. You deal with many small atoms that you plot their trajectory by adding layers. But in the end, you still basically end up with a bunch of streams.

We also know that writing reactive data carries real weight and being pragmatic we should be explicit. How do we keep a strong contract if we are letting whoever gets the data to be able to write it too?