Primitives
Solid's bread and butter is helping you author a reactive system that automatically avoids unnecessary calculations and propagates only the minimal changes out from the extremities of the system (likely involving surgical DOM updates). This is accomplished by the low-overhead auto-subscription system to track dependencies.
The Counter demo is a perfect showcase of Solid's primitives as building blocks by using them to track numbers, and more complex versions with more of the same primitives will stay performant and easy to read & write. It is a really nice synchronization mechanism where the whole app is up to date without losing time on wasted calculations or needing to update things by hand. It is a joy to use and is freeing when making architecture decisions for a codebase because the system is not derailed by component or file boundaries. You are separating pieces logically rather than to avoid performance pits.
The reactivity story of the primitives has some issues when dealing with objects. Take this example:
const [user, setUser] = createSignal({
name: 'Frank',
location: "Germany"
});
createEffect(() => console.log('name:', user().name));
createEffect(() => console.log('location:', user().location));
// TODO: Update location without re-triggering a name log.
How should we update the location without triggering a duplicate name log? Calling setUser with a new object will re-trigger the name log, and if we modify the existing object the reactive system would not understand it needs to run the location log.
Although it may be somewhat incongruent with the reactive system's mechanics or performance goals, sometimes we need to diff objects. Enter the store.
solid-js/store
The Proxy API is well used here. Sub-object reads are detected by proxy getter and give back a generated nested reactive signal, while sub-object updates are detected either by proxy setter as in createMutable or by explicit set function separation as in createStore and in both cases the new value is diffed against the previous object state to trigger sub-object signals.
const [user, setUser] = createStore({
name: "Frank",
location: { country: "Germany", city: "Munich" },
});
createEffect(() => console.log("name:", user.name));
createEffect(() =>
console.log("country:", user.location.country)
);
createEffect(() =>
console.log("city:", user.location.city));
setUser(
"location",
{country: "Germany", city: "Berlin" }
);
// Output
// name: Frank
// country: Germany
// city: Munich
// city: Berlin
This is a nice API and it works well given the seemingly unavoidable cost of diffing. However, it does not always play well with the basic primitives from before:
const [files, setFiles] = createSignal<File[]>(someFiles);
const [selectedIndex, setSelectedIndex] = createSignal(0);
const parsedFile = createMemo(() =>
parse(files()[selectedIndex()])
);
// parsedFile is not deeply reactive so we must send it
// into a store by using the discouraged practice of
// synchronization.
const [fileStore, setStore] = createStore({
file: parse(someFiles[0])
});
createEffect(() => setStore('file', parsedFile()))
The story is similar for updating one store reactively from the value of another store: I can't tell a signal to be deeply reactive by diffing, so I must construct a second reactive system (a store) and synchronize it with my first system (createEffect / createComputed).
Authoring choices
After running into the above situation, I noticed similar issues elsewhere. For example, I want to write sections of my app as self-contained modules, and then connect the resulting signals somewhere else:
// cardPicker.tsx
const [selectedCard, setSelectedCard] = createSignal<Card>();
// ...some UI to populate via setSelectedCard.
// magicTrick.tsx
const [audienceCard, setAudienceCard] = createSignal<Card>();
// ...some UI to show a trick using audienceCard() signal.
// App.tsx
// Now I am defining the relationship between app sections.
createEffect(() => setAudienceCard(selectedCard()));
It is probably always possible to redo the systems into a single system where everything is derived within a single reactive system, but this is introducing design constraints to using a library that just lifted other authoring constraints from my mind.
A mature framework
Synchronization is an important aspect of app development that deserves to be well integrated into frameworks even though allowing only a single system has more potential for optimization.
Solid has reached a really nice place by extending and improving its core reactivity mechanisms but it needed to make further decisions in less clearcut areas for SolidStart that don't follow from the reactivity axioms. It will need to continue pick sides on softer decisions that might turn out to be problems later. Other frameworks are going through this work now to introduce a reactivity system into their existing APIs.
As Solid transitions into a mature long-term framework, it is going to need to choose a philosophy for how to react to new ideas in the industry that don't mesh into its existing APIs and algorithms. In the Solid community I see the usual "new tool emphasizes established tool's legacy cruft": poking fun at Vue for supporting new ways and old ways together, or at React for sticking with their existing model, or at Angular for changing a lot with breaking changes, but really those are the only options. Solid is great but "get everything right from the start" will always run out, and part of a mature tool is picking what is encouraged and what they will make the user go through escape hatches to accomplish.
Top comments (0)