This article delves into the features and concepts of SolidJS, a JavaScript library for building user interfaces, comparing it to React and exploring its core concepts, ecosystem, and advanced guide.
Abstract
The article begins by introducing SolidJS, a JavaScript library for building user interfaces, and comparing it to React. It then explores the core concepts of SolidJS, including JSX for UI, reactive system, immutability, life cycle, and scoped CSS. The article also discusses the performance and adoption of SolidJS, referencing benchmarks and real-world production environments. The ecosystem guide covers development kits, devtools, UI libraries, application testing, and documentation. The advanced guide section explains fetching data, conditional rendering, lists, navigation, rendering techniques, and optimization. The article concludes by summarizing the main features and aspects of SolidJS and providing a personal conclusion about the author's preference for future projects.
Opinions
The author appreciates the innovative concepts introduced by SolidJS and its focus on enhancing web application performance.
The author remains cautious due to SolidJS's limited adoption, sparse tooling and library support, and potential issues with bugs and compatibility.
The author would consider using SolidJS for smaller-scale projects or internal proof-of-concepts where they have the flexibility to develop from scratch and address challenges as they arise.
The author currently leans towards Svelte for future projects, given its established ecosystem and proven track record.
The author is open to discovering Qwik, another JavaScript library for building user interfaces.
The author expresses gratitude to the readers and provides their GitHub profile for further connection.
The author recommends trying out an AI service that provides the same performance and functions as ChatGPT Plus(GPT-4) but is more cost-effective.
Frontend Development Beyond React: Solid (2/3)
Delving into Svelte, Solid, and Qwik
Woman developer (like me) (Image licensed to the author)
Introduction
This article is part of a series that I will focus on studying Frontend development outside of React. After discovering Svelte and its interesting features, we will explore Solid in this article.
This article provides a reference that summarizes the important aspects of Solid, both in theory and practice.
Similarly, as a React developer for a long time, through this series, I will try to challenge what is currently in React with other approaches: if they see things differently and especially more effectively.
This article will provide you with all the knowledge I gained from my studies, and I hope that it will be beneficial and make it easier for you to start using Solid.
If you’re motivated to follow me, buckle your belts. We’re going to start right away! 🚀
Core Concepts Guide
SolidJS is built on JSX, reactivity, fine-grained reactivity, and immutability, providing a powerful foundation for creating efficient web applications. Let’s dive deeper into each of these core aspects!
JSX for UI
In both SolidJS and React, JSX is utilized to declaratively describe the structure and appearance of user interfaces:
Just like in React, JSX in SolidJS enables component composition, dynamic content rendering, event handling, and seamless integration with JavaScript expressions and logic:
import { createSignal, createEffect } from"solid-js";
import { render } from"solid-js/web";
// Child component with JavaScript logicfunctionChildComponent(props) {
const [isEven, setIsEven] = createSignal(false);
// Effect to determine if count is even or oddcreateEffect(() => {
setIsEven(props.count() % 2 === 0);
});
return (
<p><span>Count: {props.count()}</span><span>{isEven() ? " (Even)" : " (Odd)"}</span>{" "}
{/* Display whether count is even or odd */}
</p>
);
}
// Parent component managing state and passing it to ChildComponentfunctionParentComponent() {
const [count, setCount] = createSignal(0); // Signal to hold count stateconstincrement = () => {
setCount(count() + 1); // Increment count when button is clicked
};
return (
<div><h1>Parent Component</h1><buttononClick={increment}>Increment Count</button>{" "}
{/* Button to increment count */}
<ChildComponentcount={count} />{" "}
{/* Render ChildComponent and pass count state */}
</div>
);
}
render(() =><ParentComponent />, document.getElementById("app")!);
SolidJS and React both use JSX as a way to bridge the gap between JavaScript and HTML, providing a more expressive and intuitive syntax for building UI components.
Reactive System
Solid’s design is based on reactivity, which ensures that applications are synchronized with their underlying data. Updates are constrained to the modified portions, avoiding the need to refresh the entire component:
This is called Fine-grained reactivity. Fine-grained reactivity refers to the ability to update only the specific parts of a component that have changed, rather than re-rendering the entire component. This approach enhances performance and efficiency by minimizing unnecessary updates.
Signals and Subscribersare the core components of Solid’s reactive systems:
1️⃣ SolidJS maintains an internal dependency graph that maps signals to the components or elements within the view that depend on them.
// when the value of the signal value changes, // SolidJS knows that it needs to re-render MyComponent, // which in turn updates the <div> element with the new value.Signal (value) --> MyComponent --> <div>
Signals are event emitters that hold a list of subscriptions. They notify their subscribers whenever their value changes. Where things get more interesting is how these subscriptions happen. Solid uses automatic dependency tracking. Updates happen automatically as the data changes. The trick is a global stack at runtime. — https://www.solidjs.com/guides/reactivity
2️⃣ When a signal changes, SolidJS refers to the dependency graph to efficiently identify and update only the affected parts of the view.
3️⃣ SolidJS constructs its dependency graph at runtime. During the execution of a SolidJS application, when signals are created and dependencies between signals and components are established, SolidJS dynamically updates its internal dependency graph to reflect these relationships.
Below is a simplified internal implementation of the interaction between the “signal” and the “dependency graph”:
4️⃣ The structural graph represents the hierarchical structure of the UI components. SolidJS leverages the structural graph to optimize rendering and updates.
5️⃣ When a component updates, SolidJS traverses the structural graph to determine the optimal order in which to update components and render the DOM.
💡 The combination of the dependency graph and the structural graph allows SolidJS to efficiently manage reactivity and rendering.
✳️ There is another case of reactivity: Store. A store is a special type of signal that holds reactive state. While signals manage a single piece of state, stores create a centralized location to reduce code redundancy (the same purpose as using useReducer in React).
Stores are Solid’s answer to nested reactivity. They are proxy objects whose properties can be tracked and can contain other objects which automatically become wrapped in proxies themselves, and so on. — https://www.solidjs.com/tutorial/stores_createstore
JavaScript Proxies offer a versatile toolset for implementing reactive systems, enabling the interception of data access and facilitating seamless updates in response to data changes.
Using JavaScript’s proxy mechanism, reactivity extends beyond just the top-level objects or arrays. With stores, you can now target nested properties and elements within these structures to create a dynamic tree of reactive data. — https://docs.solidjs.com/concepts/stores
☑ Here’s an illustration:
Proxy for the state (stateProxy): a proxy is established for the state object via the Proxyconstructor, enabling interception of object operations like property access and assignment.
get trap: intercepts attempts to read properties of stateProxy. When a property is accessed, a logged message indicates the property being accessed, and its value is returned.
set trap: intercepts attempts to set properties of stateProxy. Upon setting a property, a logged message indicates the property and its new value before updating the property’s value.
To correctly update the state while still maintaining the proxy behavior (i.e., triggering the set trap), we need to update the properties of the existing stateProxy object one by one. This way, each property assignment will go through the proxy, allowing the set trap to intercept and handle the update (thereby "stateProxy = newState" is incorrect).
☑ We can modify the code to allow attaching and triggering callbacks whenever the state changes:
In this updated code:
We’ve added an array called subscribers to hold callback functions.
In the set trap of the proxy, after updating the state, we trigger all the callbacks (subscribers) with the updated state.
We’ve added a subscribe function to allow attaching callback functions to be called whenever the state changes.
We’ve subscribed to state changes using the subscribe function and provided a callback function that logs the updated state to the console.
Now, whenever the state changes, all subscribed callback functions will be triggered with the updated state.
☑ To make a React functional component subscribe to state changes in the reactive store, here’s how we can adjust the code:
The Counter component renders the count from the store's state.
It uses the useState hook to manage local state for the component.
The useEffect hook is used to subscribe to state changes and update the local state accordingly when the component mounts.
Another useEffect hook increments the count in the store's state every 5 seconds using setInterval.
☑ In SolidJS, this code can be replaced by:
import { onCleanup } from"solid-js";
import { createStore } from"solid-js/store";
import { render } from"solid-js/web";
// Create a store with initial stateconst [store, setStore] = createStore({ count: 0 });
// Define the Counter componentfunctionCounter() {
// Set up an interval timer to increment the count every 5 secondsconst timer = setInterval(() => {
setStore((prev) => ({ ...prev, count: prev.count + 1 }));
}, 5000);
// Clean up the interval timer when the component unmountsonCleanup(() => {
clearInterval(timer);
});
// Render the count from the storereturn<span>Count: {store.count}</span>;
}
// Render the Counter component to the DOMrender(() =><Counter />, document.getElementById("app")!);
We create a store using createStore with an initial state { count: 0 }.
The Counter component sets up an interval timer to increment the count in the store every 5 seconds.
Babel Plugin JSX DOM Expressions: This package is a JSX compiler built for DOM Expressions to provide a general JSX to DOM transformation for reactive libraries that do fine grained change detection. This package aims to convert JSX statements to native DOM statements and wrap JSX expressions with functions that can be implemented with the library of your choice. Sort of like a JSX to Hyperscript for fine change detection. — https://github.com/ryansolid/dom-expressions/tree/main/packages/babel-plugin-jsx-dom-expressions
DOM Expressions: DOM Expressions is a Rendering Runtime for reactive libraries that do fine grained change detection. These libraries rely on concepts like Observables and Signals rather than Lifecycle functions and the Virtual DOM. Standard JSX transformers are not helpful to these libraries as they need to evaluate their expressions in isolation to avoid re-rendering unnecessary parts of the DOM. — https://github.com/ryansolid/dom-expressions/tree/main/packages/dom-expressions
💡 In summary, SolidJS prioritizes reactivity and directly manipulates the DOM for efficient updates, unlike React’s virtual DOM. This approach ensures fine-grained reactivity without performance drawbacks and emphasizes functional programming, offering tools like signals and memoization for state management and rendering optimization.
Immutability
Immutable data structures are ones where the contents cannot be modified after creation. This approach promotes predictability and helps prevent unintended side effects in applications.
2️⃣ onCleanup: This method is called when a component is unmounted from the DOM. It’s useful for performing cleanup tasks, such as removing event listeners or subscriptions.
import { createSignal, onCleanup, onMount } from"solid-js";
import { render } from"solid-js/web";
exportconstApp = () => {
// Create a signal to store the window dimensionsconst [rect, setRect] = createSignal({
height: window.innerHeight, // Initialize height with current window heightwidth: window.innerWidth, // Initialize width with current window width
});
// Event handler to update dimensions when window is resizedconsthandler = (event: Event) => {
setRect({ // Update signal with new dimensionsheight: window.innerHeight, // Update height with new window heightwidth: window.innerWidth, // Update width with new window width
});
};
// Add resize event listener when component mountsonMount(() => {
window.addEventListener("resize", handler); // Listen for window resize events
});
// Remove resize event listener when component unmountsonCleanup(() => {
window.removeEventListener("resize", handler); // Remove event listener to avoid memory leaks
});
// Render the component with window dimensionsreturn<span>Window Dimensions: {JSON.stringify(rect())}</span>;
};
// Render the App component into the document bodyrender(() =><App />, document.body);
3️⃣ createEffect: This function creates an effect that runs when the component renders or when its dependencies change. It’s similar to useEffect in React and is often used for handling side effects.
import { render } from"solid-js/web";
import { createEffect, createSignal } from"solid-js";
constMyComponent = () => {
const [count, setCount] = createSignal(0);
createEffect(() => {
// each time the count changes, // it will log the updated count to the consoleconsole.log("Count changed:", count());
}); // without dependencies listreturn (
<div><buttononClick={() => setCount(count() + 1)}>Increment Count</button></div>
);
};
render(() =><MyComponent />, document.getElementById("app")!);
4️⃣ createMemo: This function creates a memoized value (derived value) based on other reactive values. It only recalculates when its dependencies change.
import { createMemo, createSignal, For } from"solid-js";
import { render } from"solid-js/web";
// Sample list of itemsconst items = [
{ id: 1, name: "Apple" },
{ id: 2, name: "Banana" },
{ id: 3, name: "Orange" },
{ id: 4, name: "Grapes" },
{ id: 5, name: "Pineapple" },
];
constApp = () => {
// State to store the search queryconst [searchQuery, setSearchQuery] = createSignal("");
// Memoized value for filtered items based on search queryconst filteredItems = createMemo(() => {
const query = searchQuery().toLowerCase();
if (!query) return items; // Return all items if search query is emptyreturn items.filter((item) => item.name.toLowerCase().includes(query));
});
// Function to handle search query changeconsthandleSearchChange = (e: { target: { value: any } }) => {
setSearchQuery(e.target.value);
};
return (
<div><inputtype="text"placeholder="Search items..."onInput={handleSearchChange}
/><ul><Foreach={filteredItems()}>{(item) => <li>{item.name}</li>}</For></ul></div>
);
};
render(() =><App />, document.getElementById("app")!);
All examples are functional. You can test them in the playground here.
In Solid, you often don’t need to wrap functions in memos; you can alternatively just define and call a regular function to get similar reactive behavior. The main difference is when you call the function in multiple reactive settings. In this case, when the function’s dependencies update, the function will get called multiple times unless it is wrapped in createMemo. — https://docs.solidjs.com/reference/basic-reactivity/create-memo
Scoped CSS
SolidJS does not natively support scoped CSS like Svelte does. However, we can achieve scoped CSS in SolidJS using CSS Modules:
When we use CSS Modules, the class names defined in the CSS files are automatically transformed during compilation. Each class name is given a unique identifier, typically a hash or a random string, to ensure its uniqueness across the application.
The CSS classes defined in “MyComponent.module.css” are scoped to the “MyComponent” component. They are imported into the component file and applied to the corresponding JSX elements using the class attribute. This ensures that the styles only affect the elements within the component, avoiding unintended side effects.
A stopover to evaluate performance and adoption
At this point, I am pondering many questions about execution (runtime) performance. Is it more efficient to compare the DOM and make a diff/patch (VDOM and reconciliation), or to maintain two graphs (dependency graph and structural graph), especially for large and complex views? I will refer to benchmarks and SolidJS usage in real-world production environments.
These benchmarks show that SolidJS is more efficient in memory, CPU, and network than React.
Let’s now examine its use in real-world production environments. Throughout my investigation, I encountered only two notable applications that utilize SolidJS:
🔻 The limited adoption of SolidJS in production raises concerns regarding its maturity, stability, and reliability. This situation could potentially hinder the framework’s development, affecting its capability to manage diverse workloads, tackle challenges, and foster innovation.
Now that we’ve gained insight into Solid’s internal mechanisms, let’s put our knowledge into practice. 💻
Ecosystem Guide
Development Kits
SolidStart is an open source, meta-framework that provides the platform to put components that comprise a web application together. It is built on top of SolidJS and uses Vinxi, an agnostic Framework Bundler that combines the power of Vite and Nitro. — https://docs.solidjs.com/solid-start
For the development of inclusive and user-friendly web applications, it is crucial to guarantee accessibility and semantic HTML. Here are the libraries I kept after selection:
✳️ Kobalte is a UI toolkit for building accessible web apps and design systems with SolidJS.
I admit that research and selection were difficult. The majority of libraries are either not complete, not accessible, or deprecated.
Application testing
In the SolidJS official GitHub repository, I discovered this library: solid-jest.
Nevertheless, there is no documentation in the Readme on how to use it and some examples. I recommend that you see and follow this article accordingly.
I could not validate other tools because I did not find official documentation (Cypress, Playwright, etc.).
Documentation
I was confused by the old and new documentation, to be honest.
It’s evident that the SolidJS ecosystem is continuously evolving, with ongoing efforts to refine and expand its features and capabilities.
Advanced Guide
Fetching Data
createResource is a utility provided by SolidJS for managing asynchronous data fetching and caching:
Be careful, the SolidJS components are rendered only once, you must use the Match, Switch, Show tags to manage the display according to the loading status: loading, error and data. Hence, doing this is not correct:
<For> is designed to be used when the order and length of the list may change frequently. When the list value changes in <For>, the entire list is re-rendered. However, if the array undergoes a change, such as an element shifting position, <For> will manage this by simply moving the corresponding DOM node and updating the index. […] If you were using <For>, the entire list would be re-rendered when a value changes, even if the length of the list remains unchanged. — https://docs.solidjs.com/concepts/control-flow/list-rendering#index-vs-for
Navigation
If you go through SolidStart, the routing is managed automatically. Otherwise, Solid Router simplifies routing in Solid applications:
However, there is no framework like NextJs for SolidJS, if I’m not wrong. Therefore, everything should be done manually from the beginning.
Furthermore, let us take a look at this article written by the creator of SolidJS: 5 Places SolidJS is not the Best. Among the points, there is SSR:
While we haven’t seen a framework successfully pull this off as of yet, nor have an architecture granular enough it can leverage it, it will happen. For now, Solid will just have to be satisfied being the fastest raw renderer in the server and the browser, with the knowledge that it is theoretically possible that other frameworks will outperform it on the server one day. — https://dev.to/this-is-learning/5-places-solidjs-is-not-the-best-5019
Once again, it is evident that SolidJS is not as mature as React.
Optimization Guide
Handling large data sources
List virtualization, also known as ‘windowing’, is a highly efficient method for presenting extensive lists. I’m looking for libraries like react-window and react-virtualized.
Solid’s lazy method allows to wrap the component's dynamic import for deferred lazy loading. Suspense serves as a boundary that can display a fallback placeholder instead of the partially loaded content as these async events resolve:
Browser support for this option can be found here.
However, this GitHub thread discusses an issue with image lazy loading in SolidJS, specifically related to how the loading="lazy" attribute behaves within SolidJS applications. Users have reported that using this attribute does not trigger lazy loading as expected, leading to images being loaded immediately instead of deferring loading until they enter the viewport.
The issue is always open. No suggestion or solution since 27 days.
This is somewhat upsetting. I’m wondering if there are any other compatibility issues because SolidJS isn’t used much in production (less feedback).
Summary
Main features and aspects of SolidJS (Image by the author)Pros and cons of SolidJS (Image by the author)
Conclusion
I appreciate the innovative concepts introduced by SolidJS and its focus on enhancing web application performance. However, I remain cautious due to its limited adoption, sparse tooling and library support, as well as potential issues with bugs and compatibility.
For smaller-scale projects or internal proof-of-concepts where I have the flexibility to develop from scratch and address challenges as they arise, I would consider using SolidJS.
However, when asked what is my preferred framework for future projects (besides React), I would currently lean towards Svelte, given its established ecosystem and proven track record. I keep this choice until soon discover Qwik.
Until we meet again in a new article and a fresh adventure! ❤️
Thank you for reading my article.
Want to Connect?
You can find me at GitHub: https://github.com/helabenkhalfallah