Re-evaluating Boundaries

We already have established that Components are the de facto way to break apart your code. In a reactive library, execution is defined by a series of event handlers. These handlers form a graph in their own right. The easiest tact would be to align the granularity with Components. This is the approach MobX uses with React, and how Vue works essentially. Both of these libraries feed into Virtual DOM diff engines where Components serve as nodes. Svelte works similarly as well. It compiles Components to locally optimized modules.

But on the other hand, from a performance standpoint, Components aren’t necessarily the right boundaries. Components may subdivide static templates, and they may contain structurally dynamic sections. Creating the former and updating the latter can impact your application's performance. So for Solid, I knew the handling of Components was going to be critical both for a developer experience standpoint defining real-world performance.

So the very first thing I did with Solid was to attempt to make Components very little more than a function call. They are essentially DOM/reactive graph factories. They create DOM nodes and create reactive computations that create closures over the reactive data points and those DOM nodes. After they are created the Component has no purpose as the system works on its own. It seems simple enough. However, using this pattern effectively with a reactive system that doesn’t have bounds at the Component isn’t always a good development experience or intuitive. Consider using one of Solid’s state proxies:

<MyComponent name={state.user.name} />

And you might define this Component as:

const MyComponent = props => <h2>Hi, {props.name}</h2>

Now if this was just a function call it would would expect the compiler to output something like this:

MyComponent({name: state.user.name});

However, there is a problem with that. We are resolving the state down to a simple value at call time at which point it is no longer reactive. Worse it can be tracked by its owning context causing the whole owning context to re-evaluate on value change. Unsurprisingly, the answer is to wrap it in a function. But that brings its own issues. Do you really want to access name as props.name() ? And more so what happens if you want your Component to accept simple values?

<MyComponent name="Jack Smith" />

No. I wanted to avoid the Component writer to have to check if they are receiving a function or not. One tact I’ve seen commonly in the past in these sorts of libraries is wrapping each binding in a computation that writes to a single props observable or to a proxy props object. But that is heavy. Still doing nothing isn’t acceptable. Where I landed was to use the compiler to detect dynamic expressions and wrap those in functions while leaving simple values as a direct assignment. Then take those wrapped expressions and set them as a getter on the props object. This is handled by an internal compiler generated createComponent method, where the 3rd argument is keys that should be wrapped as a getter.

createComponent(MyComponent, {name: () => state.user.name}, ["name"]);

What this means is that state.user.name is not evaluated until props.name is accessed so that it is tracked by the context where props.name is being used. That is not until the Component internal nodes are created and bound that the value is accessed. This ensures no dependencies are made until the last minute. MobX has this similar consideration of not accessing props until where they are being bound but with React there is no simple solution here. However, this simple function wrapper consistently presented behind an Object getter, lets us pack things up without creating any additional reactive nodes.

This pattern of deferred evaluation is also helpful for handing props.children as for things like conditionals or templating(render props) the Component can evaluate the children at its discretion. This gives an incredible amount of power to Components.

There is a second problem with just using functions. Consider a conditional:

<div>{state.show && <MyComponent />}</div>

This would roughly compile down to:

const el = document.createElement('div');

createEffect(() => el.appendChild(state.show && MyComponent({}));

The problem here is that now <MyComponent /> is being executed inside a tracking context. Any sort of incidental access of a reactive property would trigger the whole conditional again. createComponent also ensures that the Component is created in a non-tracked context.

With this at our disposal we can basically allow the component to disappear after execution with the minimal overhead of a function call and setting a few getters on a props object. No new reactive nodes are created which are where the real expense is. However, even though we have detached the reactive lifecycle from the Components it still doesn’t help us optimize at that ideal static template level. We still are splitting apart our code.