From ReactJS to SolidJS
Foreword
In this article I want to talk about the SolidJS library for creating interactive interfaces, which many of you are probably already familiar with, and my experience in using it in real applications.
Everything described here is based on my two years of experience with the library. Perhaps some of the statements may seem controversial to you, so I want to point out that everything I write here is subjective.
My aim is to share with developers a great tool that can ease the creation of interfaces, speed up their performance, and reduce the labor cost of developing new functionality.
About me
My name is Vlad, I’ve been doing frontend for about 6 years. During this time, I have worked both in large companies (Yandex, Arrival) and small startups (The Bricks Inc, Own.Space). I tried a huge number of tools and libraries, so I’m familiar with different approaches. For about 5 years I’ve been developing with React. I liked the very approach of this library, because it gave me more freedom (using pure JS instead of special syntax, such as Vue, Svelte) and control over the code at the same time. By control I mean that in the output I get what I wrote: for example, Vue and Svelte compile the code that the developer writes into JS. I was committed to React for quite a long time, until I found out about SolidJS…
Why do we need new technologies?
You may ask, “Why do I need to learn something else when there are already solutions on the market that allow us to develop interfaces conveniently enough?” It’s a reasonable question, since you can (but do you need to?) make do with one solution. In fact, people don’t really care what technology you use: JQuery, Angular, React, Ebmer, VanillaJS — it doesn’t matter. We need to remember that we make interfaces for people, so our task is to make them convenient, fast, functional and useful, so our users can quickly find what they need, so the battery on phones or laptops last longer, so the browser consumes less memory, allowing people to save money on devices, so users enjoy using our products and they will return to us again. But developers are people too! That’s why we want to reduce the effort involved in developing and maintaining functionality and increase developer satisfaction with the process, so that no one burns out or quits. In this case business will be satisfied, because it will be possible to iterate quickly over the product, reduce material costs on resources and increase profit and attractiveness of the product on the market. In short, one wants everyone to be happy! Of course, this is an ideal world, but we can strive for it, and new technologies and approaches can bring us closer to it.
SolidJS was the technology that made life easier for me and the other developers on my team, made it easier to create new functionality, reduced development time, and greatly increased my level of satisfaction with the process. As a bonus, we got great performance, but more on that later.
SolidJS — what? Another framework?
I well remember that when I was just starting my frontend career, my eyes lit up brightly after news about new libraries or tools. The new “SUPER-MEGA-TOOL.JS” came out — urgently rewrite everything on it! Probably, many people with a smile recognize themselves in the past :) Time passed, my knowledge of tools multiplied, I tried to implement new libraries into my projects (I especially liked experimenting with state managers, as I never had the pleasure of writing reducer-functions and actions for redux — it seemed to me that it could be done somehow easier). Eventually my enthusiasm for new tools waned, and I became more cautious about tools and technologies. At some point I settled on a bunch of React and Zustand (if there was a need for a global repository).
I’m a big fan of the web and everything that happens with the web, so I read various articles on web development quite often. I firmly believe that the web is like medicine — you need to keep up to date with new technologies in order to improve your interface design processes and, as a result, improve the interfaces themselves, since human-computer interaction is a critical issue nowadays. One evening I came across an article about SolidJS. I was interested in the new concept of “signals”, so I decided to see what it was.
Have we met?
So, SolidJS is not a new library. It has been in development for more than 6 years, but relatively recently started its crusade in the world of frontend technologies. This library will be closer to React-developers, because it largely borrowed the syntax of React, but the principle of work has been radically revised and changed.
Let’s analyze a concrete example.
import { render } from "solid-js/web";
import { createSignal } from "solid-js";
function Counter() {
const [count, setCount] = createSignal(1);
const increment = () => setCount(count() + 1);
return (
<button type="button" onClick={increment}>
{count()}
</button>
);
}
render(() => <Counter />, document.getElementById("app"))
Doesn’t it remind you of anything?
Of course, we can see that React has influenced SolidJS quite a bit (which is a big plus, which I’ll talk about later). The difference here, however, is that count is a function (signal) and not a number. When we call count, we get the last value of the signal. The differences don’t end there though…
The main difference is that when the signal is updated, the function doesn’t run again, i.e., there is no re-rendering! SolidJS doesn’t work with the virtual DOM, it updates it directly. I’d be deceiving myself if I didn’t say that there’s a share of compilation here, too. JSX is compiled to a more optimized form, allowing you to work with the DOM granularly, which means that after compiling the code above we get the following:
import { template as _$template } from "solid-js/web";
import { delegateEvents as _$delegateEvents } from "solid-js/web";
import { createComponent as _$createComponent } from "solid-js/web";
import { insert as _$insert } from "solid-js/web";
import { render } from "solid-js/web";
import { createSignal } from "solid-js";
const _tmpl$ = /*#__PURE__*/_$template(`<button type="button">`);
function Counter() {
const [count, setCount] = createSignal(1);
const increment = () => setCount(count() + 1);
return (() => {
const _el$ = _tmpl$();
_el$.$$click = increment;
_$insert(_el$, count);
return _el$;
})();
}
render(() => _$createComponent(Counter, {}), document.getElementById("app"));
_$delegateEvents(["click"]);
You can see that SolidJS compiles only JSX without modifying your code, which is outside JSX. This allows developers to write familiar code without having to learn low-level library constructs.
Basics
Consider the following example. How many times will we see the word click in the console if we press the button?
function Counter() {
const [count, setCount] = createSignal(1);
const increment = () => setCount(count() + 1);
console.log('click');
return (
<button type="button" onClick={increment}>
{count()}
</button>
);
}
Only once! It turns out that the function in JS works the way it should work — only once. Do you have any idea what possibilities this opens up?
And now let’s add some effects. Let’s say we want to output to the console the number of clicks made by the user. In React we would want to add something like:
useEffect(() => console.log(`Clicked: ${count}`), [count])
In SolidJS we’ll do about the same thing:
createEffect(() => console.log(`Clicked: ${count()}`))
Wait a second, where is specifying dependencies for an effect? The thing is that in SolidJS you don’t need to specify dependencies, because they automatically start tracking the moment you use the signal inside the effect (you can read more about how reactivity works in SolidJS here).
You can clean up effects with onCleanup.
Let’s look at another example:
function Counter() {
const [count, setCount] = createSignal(1);
const increment = () => setCount(count() + 1);
const doubled = () => count() * 2;
return (
<div>
<button type="button" onClick={increment}>
{count()}
</button>
{/* Instead of doubled() we could use count() * 2 */}
<span>Doubled clicks: {doubled()}</span>
</div>
);
}
Here we can see the doubled number of clicks. So why is doubled
a function? Remember that the Counter function is only called once, so if we wrote:
const doubled = count() * 2;
You would just get the number 2, which does not update when you change the count
signal.
We can create a memoized value:
const doubled = createMemo(() => count() * 2);
Now, if you call doubled()
in multiple places, the value will only be counted once and will only be recalculated when the count
signal changes. Note that you don’t need to specify dependencies here either: everything is taken into account by-design.
In addition to simple signals, we can create Stores.
import { createEffect } from "solid-js";
import { createStore } from "solid-js/store";
export function Form() {
const [state, setState] = createStore({
name: "Vlad",
age: 28,
});
createEffect(() => console.log(`Name: ${state.name}, age: ${state.age}`));
return (
<form>
<label>
Name:{" "}
<input
type="text"
value={state.name}
onInput={(e) => setState({ name: e.target.value })}
/>
</label>
<label>
Age:{" "}
<input
type="number"
value={state.age}
onInput={(e) => setState({ age: e.target.value })}
/>
</label>
</form>
);
}
In this example, state
is a Proxy object. Because of this, the effect will work every time we change the age field or name field in the state
object. Remember the class components in React? It’s very similar. But now you can create handy forms that won’t re-render our component!
I want to pay attention to the event onInput
. Why not onChange
? The point is that onChange
is a synthetic React event. Because SolidJS relies on native events, you should use onInput
.
State management
Suppose we want to create a global store, which library should we use? React doesn’t provide us with a convenient way to manipulate data, so countless state management libraries have been created to try to solve this problem. For SolidJS you can use… SolidJS!
const [count, setCount] = createSignal(1);
function Counter() {
const increment = () => setCount(count() + 1);
return (
<button type="button" onClick={increment}>
{count()}
</button>
);
}
// Somewhere inside a component
<Counter />
<Counter />
<Counter />
In the example above, all three buttons would share a common state. That’s because we can use the SolidJS API anywhere (it’s important to remember to clean up the memory afterwards, you can create global stores for that with createRoot
)!
You can use createStore
along with createSignal
for your global stores.
So, no more looking for a nice state management solution because SolidJS allows you to manage the state conveniently out of the box.
How to async?
And now the fun part: how do we handle async? To do this we need createResource
(no equivalent in React) and Suspense
(which landed in React later than in SolidJS).
Let’s look at an example.
import { createSignal, createResource } from "solid-js";
export const AsyncExample = () => {
const [id, setId] = createSignal(1);
const [data, { mutate, refetch }] = createResource(
// Here we can pass a signal
// which will trigger the fetcher function
id,
// The first argument is the latest signal value
async (currentId) =>
await fetch(
`https://jsonplaceholder.typicode.com/todos/${currentId}`
).then((res) => res.json())
);
return (
<section>
<div>
<Suspense fallback={<span>Loading...</span>}>{data()?.title}</Suspense>
</div>
<button type="button" onClick={() => setId((id) => id + 1)}>
Increase id
</button>
</section>
);
};
In this case, we will make a query with id === 1
and render the title
field of the data
signal. But what will be displayed while there is no data yet? The thing is the Suspense
component creates an area where it tracks resources (createResource
). As long as they are not yet committed, we will see the fallback
. The first argument I pass to createResource
is the signal id
on which the request depends (fetcher function). This value is substituted into the variable currentId
, and then the query is executed again.
If we don’t need signal, on which fetcher function depends, we don’t need to pass anything at all. Then we get the following:
const [data, { mutate, refetch }] = createResource(
async () =>
await fetch(
`https://jsonplaceholder.typicode.com/todos/1`
).then((res) => res.json())
);
The mutate
and refetch
functions allow for manual control. Read more about createResource
here.
There’s more.
Now let’s think about what SolidJS has to offer:
Native Events
SolidJS works with native browser events, but to improve performance SolidJS delegates events to reduce the number of event listeners. You know how events work in the browser — you know how they work in SolidJS. Great: less abstraction is less of a hassle.
You can forget about React.memo and performance optimization and focus on business logic
Performance will now depend only on your code, not on the library (finally the algorithms that are so often asked at job interviews will come in handy :) ). This is possible thanks to the absence of re-renders. Each function is only run once, so you can create components right in components!
DOM elements are DOM elements.
Which means that now we’ll use class
instead of className
.
// Somewhere inside a component
const div = <div></div>;
console.log(div); // >HTMLDivElement
No more hook rules
You can call hooks whatever you want, because they are just functions. You can even create effects in effects!
createEffect(() => {
// If logIsNeeded becomes false
// subscription to signalToLog will disappear
if (logIsNeeded()) {
createEffect(() => {
// Log this when signalToLog changes
console.log(signalToLog())
})
}
})
// Of course, there's more simple way
createEffect(() => {
if (logIsNeeded()) {
console.log(signalToLog())
}
})
Contexts don’t make the whole subtree re-render
And thank you for that! How often have we wanted to create a state high in the component tree and store the data we need across the entire application, but denied ourselves because of performance issues?
You know how to work with contexts in SolidJS (the syntax is exactly the same) and you can use them painlessly!
const MyContext = createContext();
const A = () => {
const [state, setState] = createSignal(0);
return <MyContext.Provider value={{state}}>
... component tree
</MyContext.Provider>
}
const B = () => {
const {state} = useContext(MyContext);
return <div>{state()}</div>
}
There are many more useful features, which you can find here.
SolidJS makes life a lot easier because, unlike React, you no longer have to struggle with the framework. For example, using React, I wrote code that was responsive to its demands, but had absolutely nothing to do with business logic. Below are just a few examples:
- “I need to wrap a component in a
memo
to improve performance, but that entails wrapping all props for that component inuseMemo
/useCallback
(which in turn can lead to similar limitations).” - “I want to pass data through context, but changing that data will cause the entire subtree to re-render, which will negatively affect performance, so I need to come up with some solution (or change the architecture) to get around that.”
- “
useEffect
inStrictMode
works differently in different versions of React, so I need to take that into account. SometimesuseEffect
is triggered twice in dev mode, so I need to somehow ignore possible issues when requesting data.” - “I need to negotiate with the team and decide on approaches and tools to create a global store, since React doesn’t provide a convenient way out of the box.”
Thanks to SolidJS, I was able to focus specifically on developing useful functionality without sacrificing performance or architecture.
Can I migrate to Solid?
It’s important for businesses to have a reliable development tool that is suitable for production, and to have developers who know and love the specific tool. With a React-like syntax, you have access to a huge army of React developers who already know Solid (and so do you)!
According to State of JavaScript 2022: Front-end Frameworks (stateofjs.com) SolidJS led the Retention category for the second year in a row. You can check out the other categories at the link. This shows that people like using SolidJS.
Below I’ll look at other very important aspects.
Performance
So, what we get is a really expressive tool for creating interfaces (not only interfaces). You might ask, “Why switch to SolidJS if there’s another X framework coming out later that’s faster?”
Let’s look at the performance.
We can see that SolidJS is very close to VanillaJS, so there is very little chance that you will have to switch to another more powerful framework in the future. Moreover, with the release of Solid 2.0 performance will increase even more due to the redesigned reactive system. And all this is 7.7 kB in GZIP.
Ecosystem
What tools are available for SolidJS? Everything you need to develop production applications. There’s a huge list here, and below I’ve listed what I think are the most important things. Yes, the ecosystem hasn’t gotten as big as React yet, but it’s growing incredibly fast.
- Router
solidjs/solid-router: A universal router for Solid inspired by Ember and React Router (github.com) - Devtools
thetarnav/solid-devtools: Library of developer tools, reactivity debugger & Devtools Chrome extension for visualizing SolidJS reactivity graph (github.com) is a great tool for visualizing the dependency graph. - A set of reactive primitives
solidjs-community/solid-primitives: A library of high-quality primitives that extend SolidJS reactivity. (github.com).
We don’t want to write everything by hand, right? It would be great if other developers could write reactive primitives for us to use. - Testing tools
solidjs/solid-testing-library: Simple and complete Solid testing utilities that encourage good testing practices. (github.com) - Integration with Storybook
elite174/storybook-solid-js: The basic setup of storybook for solid-js (github.com) - SSR framework
What is SolidStart? (solidjs.com)
For React there’s Next.JS. For Solid there’s SolidStart. And here’s the small annoying thing: as of the date of this writing (19.05.2023) SolidStart is in beta. You can use it, but you have to keep this fact in mind. If you really need SSR solution, I advise you to check out Astro which has integration with SolidJS. - Solid-query (the analogue of react-query!)
Solid Query | TanStack Query Docs - Styled-components
solidjs/solid-styled-components: A 1kb Styled Components library for Solid (github.com) - Transition group
solidjs-community/solid-transition-group: SolidJS components for applying animations when children elements enter or leave the DOM. (github.com) - Playground
Solid Playground (solidjs.com) - UI Libraries
SolidJS · Реактивная Javascript-библиотека
Many existing frontend solutions already add or have added support for Solid.
Conclusion
After 2 years of usage, I can say that SolidJS is good for production development. My team was very happy with the switch from React to SolidJS, we increased performance, migrated the code base quite easily and simplified the code a lot.
I do all my projects on Solid now because I like to write less code. I just like the way it works.
Useful Links
- Ryan Carniato’s Streams Ryan Carniato — YouTube
Here you can listen to his video streams where he explains in details how Solid works, as well as other frameworks and technologies. I highly recommend for watching (I’ve watched almost all of them myself)! - Articles by Ryan Carniato — Ryan Carniato — DEV Community
- SolidJS API
- Interactive tutorial — Reactive Javascript Library tutorial
- Solid Playground (solidjs.com)