Solid.js feels like what I always wanted React to be
Nick Scialli
February 27, 2022
Edit: thanks all for checking out this post! If you think Solid looks as promising as I do, please consider visiting the Solid.js GitHub repo and giving them a “Star” for their great work.
I started working with React professionally about three years ago. This was, coincidentally, right around when React Hooks came out. I was working in a codebase with a lot of class components, which always felt clunky.
Let’s take the following example: a counter that increments every second.
class Counter extends React.Component {
constructor() {
super();
this.state = { count: 0 };
this.increment = this.increment.bind(this);
}
increment() {
this.setState({ count: this.state.count + 1 });
}
componentDidMount() {
setInterval(() => {
this.increment();
}, 1000);
}
render() {
return <div>The count is: {this.state.count}</div>;
}
}
That’s a lot of code to write for an auto-incrementing counter. More boilerplate and ceremony means a higher likelihood for errors and worse developer experience.
Hooks are pretty neat, but error-prone
When hooks showed up, I was very excited. My counter could be reduced to the following:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
setInterval(() => {
setCount(count + 1);
}, 1000);
}, []);
return <div>The count is: {count}</div>;
}
Wait, that’s not actually right. Our useEffect
hook has a stale closure around count
because we haven’t included count
in the useEffect
dependency array. Omitting variables from dependency arrays are such a common mistake with React hooks that there are linting rules that will yell at you if you forget one.
I’ll get back to that in a moment. For now, we’ll add our missing count
variable into the dependency array:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
setInterval(() => {
setCount(count + 1);
}, 1000);
}, [count]);
return <div>The count is: {count}</div>;
}
But now we have another problem, as we take a look at running app:
Those of you more fluent in React likely know what’s going on because you battle this kind of thing every day: we’re creating too many intervals (a new one each time the effect is re-run, which is every time we increment count
). We can solve this problem a few different ways:
- Return a cleanup function from
useEffect
hook that clears the interval - Use
setTimeout
instead ofsetInterval
(good practice still to use a cleanup function) - Use the function form of
setCount
to prevent needing a direct reference to the current value
Indeed any of these will work. We’ll implement the last option here:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
setInterval(() => {
setCount((count) => count + 1);
}, 1000);
}, []);
return <div>The count is: {count}</div>;
}
And our counter is fixed! Since nothing’s in the dependency array, we only create one interval. Since we use a callback function for our count setter, we never have a stale closure over the count
variable.
This is a pretty contrived example, yet it’s still confusing unless you’ve been working with React for a bit. More complex examples, which many of us encounter on a day-to-day basis, confuse even the most seasoned React developers.
Faux reactivity
I’ve thought a lot about hooks and why they don’t feel quite right. I found the answer, it turns out, by exploring Solid.js.
The problem with React hooks is that React isn’t truly reactive. If a linter knows when an effect (or callback, or memo) hook is missing a dependency, then why can’t the framework automatically detect dependencies and react to those changes?
Digging into Solid.js
The first thing to note about Solid is that it doesn’t try to reinvent the wheel: it looks a lot like React from afar because React has some tremendous patterns: unidirectional, top-down state; JSX; component-driven architecture.
If we started to rewrite our Counter
component in Solid, we would start out like this:
function Counter() {
const [count, setCount] = createSignal(0);
return <div>The count is: {count()}</div>;
}
We see one big difference so far: count
is a function. This is called an accessor and it’s a big part of how Solid works. Of course, we have nothing here about incrementing count
on an interval, so let’s add that.
function Counter() {
const [count, setCount] = createSignal(0);
setInterval(() => {
setCount(count() + 1);
}, 1000);
return <div>The count is: {count()}</div>;
}
Surely this won’t work, right? Wouldn’t new intervals be set each time the component renders?
Nope. It just works.
But why? Well, it turns out that Solid doesn’t need to rerun the Counter
function to re-render the new count
. In fact, it doesn’t need to rerun the Counter
function at all. If we add a console.log
statement inside the Counter
function, we see that it runs only once.
function Counter() {
const [count, setCount] = createSignal(0);
setInterval(() => {
setCount(count() + 1);
}, 1000);
console.log('The Counter function was called!');
return <div>The count is: {count()}</div>;
}
In our console, just one lonely log statement:
"The Counter function was called!"
In Solid, code doesn’t run more than once unless we explicitly ask it to.
But what about hooks?
It turns out I solved our React useEffect
hook without having to actually write something that looks like a hook in Solid. Whoops! But that’s okay, we can extend our counter example to explore Solid effects.
What if we want to console.log
the count
every time it increments? Your first instinct might be to just console.log
in the body of our function:
function Counter() {
const [count, setCount] = createSignal(0);
setInterval(() => {
setCount(count() + 1);
}, 1000);
console.log(`The count is ${count()}`);
return <div>The count is: {count()}</div>;
}
That doesn’t work though. Remember, the Counter
function only runs once! But we can get the effect we’re going for by using Solid’s createEffect
function:
function Counter() {
const [count, setCount] = createSignal(0);
setInterval(() => {
setCount(count() + 1);
}, 1000);
createEffect(() => {
console.log(`The count is ${count()}`);
});
return <div>The count is: {count()}</div>;
}
This works! And we didn’t even have to tell Solid that the effect was dependent on the count
variable. This is true reactivity. If there was a second accessor called inside the createEffect
function, it would also cause the effect to run.
More interesting Solid concepts
Reactivity, not lifecycle hooks
If you’ve been in React-land for a while, the following code change might be really eyebrow-raising:
const [count, setCount] = createSignal(0);
setInterval(() => {
setCount(count() + 1);
}, 1000);
createEffect(() => {
console.log(`The count is ${count()}`);
});
function Counter() {
return <div>The count is: {count()}</div>;
}
And the code still works. Our count
signal doesn’t need to live in a component function, nor do the effects that rely on it. Everything is just a part of the reactive system and “lifecycle hooks” really don’t play much of a role.
Fine-grained DOM updates
So far I’ve been focusing a lot on developer experience (e.g., more easily writing non-buggy code), but Solid is also getting a lot of praise for its performance. One key to its strong performance is that it interacts directly with the DOM (no virtual DOM) and it performs “fine-grained” DOM updates.
Consider the following adjustment to our counter example:
function Counter() {
const [count, setCount] = createSignal(0);
setInterval(() => {
setCount(count() + 1);
}, 1000);
return (
<div>
The {(console.log('DOM update A'), false)} count is:{' '}
{(console.log('DOM update B'), count())}
</div>
);
}
If we run this, we get the following logs in our console:
DOM update A
DOM update B
DOM update B
DOM update B
DOM update B
DOM update B
DOM update B
In other words, the only thing that gets updated every second is the small piece of the DOM that contains the count
. Not even the console.log
earlier in the same div
is re-run.
Closing thoughts
I have enjoyed working with React for the past few years; it always felt like the right level of abstraction from working with the actual DOM. That being said, I have also become wary of how error-prone React hooks code often becomes. Solid.js feels like it uses a lot of the ergonomic parts of React while minimizing confusion and errors. I tried to show you some of the parts of Solid that gave me “aha!” moments, but I recommend you check out https://www.solidjs.com and explore the framework for yourself.
Nick Scialli is a senior UI engineer at Microsoft.