Reactivity has become increasingly popular in recent months, with many frameworks and libraries incorporating it into their core or being heavily influenced by it. Vue.js has reactivity at its core, while idiomatic Angular has adopted RxJS, and MobX has become popular among React developers as an alternative to the common Redux pattern. Reactivity has been one of the leading inspirations behind the original philosophy of React.

However, most libraries still use the VDOM or use some sort of batching process in the background to replicate responsive behavior, even when reactivity is first class.

Solid.js is a reactive library that prioritizes deep, granular reactivity and is designed to offer excellent performance and responsiveness without relying on the VDOM or batching processes. While Solid.js offers a developer experience similar to React, it requires a different approach to component reasoning. Let’s take a close look.

SolidJS in three words: reactive, versatile and powerful

Upon initial inspection, Solid.js code may appear to be similar to React code with a unique syntax. However, the API provided by Solid.js is more versatile and powerful, as it is not limited by the VDOM and instead relies on deep, granular reactivity. In Solid.js, components are primarily used to organize and group code, and there is no concept of rendering. We can explore a simple example:

Our React component is going to re-render endlessly. We render our interface, only to make it immediately stale by calling setValue. Every local state mutation in React triggers a re-rendering as components produce the nodes of VDOM. The process of re-rendering the interface is complex and resource-intensive, even with the use of the VDOM and React's internal optimizations. While React and Vue.js have implemented techniques to avoid unnecessary work, there are still many complex operations happening in the background.

Solid.js updates the value and that's it; once the component is mounted, there is no need to run it again. Unlike React, Solid.js will not call the component again. Solid.js doesn’t even care for createSignal to be in the same scope of the component:

In Solid.js, components are referred to as “vanishing” because they are only used to organize the interface into reusable blocks and do not serve any other purpose beyond mounting and dismounting. 

Solid.js provides more flexibility than React when it comes to managing component state. Unlike React, Solid.js does not require adherence to the “rules of hooks”, and instead allows developers to reason around scopes of modules and functions to determine which components access which states. This fine granularity means that only the element displaying the signal’s value needs to be updated; all the operations needed to maintain a VDOM are unnecessary.

Solid.js uses proxies to hide subscriptions within the function that displays the value. This allows elements consuming the signals to become the contexts that are actively called again. In contrast to React, Solid.js functions are similar to constructors that return a render method (like the JSX skeleton), while React functions are more like the render method itself.

Dealing with props

In Solid JS, getters are more than just a value, so to maintain reactivity, props need to be handled in a special way. Using the function deriveProps retains reactivity, while spreading the parameter object breaks it.  This process is more complex than using the spread and rest operators in React.

Note that we aren’t using the parenthesis to call for a getter in the case below:

We can also access the value directly.

 Although the process may seem familiar, the underlying mechanism is completely different. React re-renders the child components when the props change, which can cause a lot of work in the background to reconcile the new virtual DOM with the old. Vue.js  avoids this problem by doing simple comparisons of props, similar to wrapping a functional component inside React’s memo method. Solid.js propagates down the hierarchy of the signals, and only the elements that consume the signal are run again.

Side effects

Side effects are a common concept in functional programming that occur when a function relies on or modifies something outside its parameters. Examples of side effects include subscribing to events, calling APIs, and performing expensive computations that involve external state. In Solid.js, effects are similar to elements and subscribe to reactive values. The use of a getter simplifies the syntax compared to React.

In React, the useEffect hook is used to handle side effects. When using useEffect, a function that performs the work is passed as an argument, along with an optional array of dependencies that might change. React does a shallow comparison of the values in the array and runs the effect again if any of them change.

When using React, it can be frustrating to pass all values as props or states to avoid issues with the shallow comparison that React does. Passing an object is not a good solution because it may reference an anonymous object that is different at each render, causing the effect to run again. Solutions to this problem involve declaring multiple objects or being more literal, which adds complexity.

 In Solid.js, effects run on any signal mutation. The reference to the signal is also the dependence.

Just like React, the effect will be run again when the values change without declaring an array of dependencies or any comparison in the background. This saves time and work, while avoiding bugs related to dependencies. However, it is still possible to create an infinite loop by mutating the signal that the effect is subscribed to, so it should be avoided.

createEffect is to be thought of as the Solid.js equivalent of subscribing to observables in RxJS, in which we’re listening to all “consumed” observables - our signals - at the same time.React users may be familiar with how useEffect replaces componentDidMount, componentWillUnmount, and componentDidUpdate. Solid.js provides dedicated hooks for handling components: onMount and onCleanup. These hooks run whenever the component returns first or gets booted off the DOM, respectively. Their purpose in Solid.js is more explicit than using useEffect in React.

Handling slice of applications

In complex applications, using useState and useEffect hooks may not be enough. Passing down many variables between components, calling methods down deep, and keeping various elements in sync with each other can be challenging.  The shopping cart, language selector, user login, and themes are just a few examples of the many applications that require some sort of slice of state.

In React, there are various techniques available to handle complex applications. One approach is to use a context to allow descendant components to access a shared state. However, to avoid unnecessary re-renders, it is important to memoize and select the specific data needed. React provides native methods like useReducer and memoization techniques such as useMemo or wrapping components in  React.memo to optimize rendering and avoid unnecessary re-renders. 

Alternatively, many developers opt to define their Redux store and each of the slices. As Redux has evolved, modern Redux has become much easier to work with compared to its early days. Developers now have the option to use hooks and constructor functions, which handle concerns in a declarative manner. This eliminates the need to define constants, action creators, and other related elements in separate files for each slice.

Solid.js provides support for various state management libraries, and offers several  methods to implement different patterns. One useful method is the ability to wrap requests using resources.

Unlike React state hooks that hook into the virtual DOM, Solid.js signals are independent units that allow developers to write idiomatic JavaScript. This enables scoping signals in other modules and restricting access through methods, effectively turning signals into private singletons.

Hence, modules can act as slices of state, exporting public methods to interact with the data without the use of any external library. By declaring signals in a module scope, they can expose publicly available interfaces to shared state in all components. If signals were declared in components instead, they would be scoped to the function context, similar to the behavior of useState in React.

Furthermore, in Solid.js, API calls can be easily handled using the createResource method. This method allows developers to fetch data from an API and check the request status in a standardized manner. This function is similar to the createSignal method in Solid.js, which creates a signal that tracks a single value and can change over time, and the popular useQuery library for React.

While it may work to handle signals as different getters, at some point, it will be necessary to deal with complex, deep objects, mutating values at different levels, accessing granular slices, and in general operating over objects and arrays. The solid-js/store module provides a set of utilities for creating a store, which is a tree of signals to be accessed and mutated individually in a fully reactive manner. This is an alternative to stores in other libraries such as Redux or Pinia in Vue.js.

To set data in a Solid.js store, we can use the set method, which is similar to signals. The set method has two modes: we can pass an object that will be merged with the existing state, or pass a number of arguments that will explore our store down to the property or object that will be mutated.

For instance, let’s suppose that we have the store shown below:

We can set the user’s age to 35 by passing an object with the properties we want to update, along with a path that specifies where in the state tree to apply the update:

This will update the age property of the user object in the state tree. Furthermore, we can update the store object by passing an object that will be merged into the current one.

If we were to omit the user attribute as first parameter, we would replace the user object entirely:

Since the store is a tree of signals, which is itself a proxy, we can access the values directly using the dot syntax. Mutating a single value will cause the element to render again, just like subscribing to a signal value.

Store utilities

We have two useful methods to update our state. If we’re used to mutating a Redux store using the immer library, we can mutate the values in place using a similar syntax with the produce method:

The produce method returns a draft version of the original object, which is a new object, and any changes made to the draft object are tracked similarly to using immer. We can also pass a reconcile function call to setState. This is particularly useful when we want to match elements in the array based on a unique identifier, rather than simply overriding the entire array. For instance, we can update a specific object based on its id property by passing a reconcile function that matches the object with the same id:

This will update the object in the array with the same id, or add it to the end of the array if no matching object is found.

We can group multiple state updates together into a single transaction using the transaction utility method. This can be useful when we need to make multiple updates to the state atomically, such as when updating multiple properties of an object:

This will update the name and age properties of the user object in a single transaction, ensuring that any subscribers to the state will only receive a single notification of the change, rather than one for each update.

RxJS interoperability

We can easily work with both SolidJs and RxJS, another popular reactive library by using a couple of adapter functions. The reduce method we just talked about is shown as an example for subscriptions, similar to how services are handled in Angular.

From RxJS into Solid.js

We can turn any producer that exposes a subscribe method into a signal:

This directly handles subscription and cleaning up when the signal is dropped. We can define our signal by passing a function to track the value, and how to clean up. The set method emits the value to the contexts that are listening.

Turning our signals into observables

We can turn our signal into an Observable that exposes a subscription method, allowing it to act like a native RxJS observable.

Next, by utilizing the form method provided by  RxJS, we can transform our signal into a fully-fledged RxJS observable.

A Solid.js choice

Although it is relatively new, Solid.js has gained popularity among developers due to its unique features and exceptional performance. Compared to React, Solid.js provides useful tools out of the box and is as performant as frameworks like Svelte without the need for compilers. It is particularly suited for interfaces that require many updates to the DOM and is consistently fast even in complex applications handling real-time updates.

Solid.js offers a developer experience similar to React, but with cleaner methods and more choices.The library handles many different patterns and provides more transparency in code thanks to how scopes work natively in JavaScript. Unlike using hooks in React, there are no hidden behaviors when creating signals in Solid.js.

Using Solid.js with TypeScript solves many of the struggles developers face with complex applications made with React or Vue.js, reducing the time to market and time spent debugging issues with the VDOM. We would recommend it for any new project starting today.

Author: Federico Muzzo, Senior Front End Developer @ Bitrock