These last years I've past from AngularJS (v1) to Angular2+ and since I tried ReactJS I've considered it my go-to framework.
These weeks I've been trying SolidJS and when I compare it with ReactJS there are a set of things that I really like.
Direct access to DOM
There is no Virtual DOM in SolidJS
DOM are the classes and functions the browser give us to modify the HTML like
document.createElement()
anddiv.appendChild()
Virtual DOM is, quickly explained, a lot of code that React brings so we use it instead of the real DOM, that way React has full control over the browser.
// This is how we create elements in SolidJS
const myElement = <div />;
myElement.appendChild(<span />);
This in React is not possible because <div />
doesn't return the real DOM element but an internal representation.
const myElement = <div />;
// { type: 'div', key: null, ref: null, props: {}, ... }
console.log(myElement);
Direct access to DOM events
On the same line, since React hides the DOM behind Virtual DOM it also hides the events behind what they call synthetic events.
import { createRoot } from "react-dom/client";
function MyComponent() {
return (
<button onClick={(event) => {
// SyntheticBaseEvent {
// _reactName: "onClick",
// nativeEvent: PointerEvent,
// ...
// }
console.log(event);
}}>
Hi!
</button>
);
}
const root = createRoot(document.getElementById("root"));
root.render(<MyComponent />);
While Solid doesn't need that
function MyComponent() {
return (
<button onClick={(event) => {
// PointerEvent
console.log(event);
event.target.classList.add('clicked')
}}>
Hi
</button>
);
}
document.body.appendChild(<MyComponent />);
Signals vs Hooks
On React Hooks are used which are "magically" connected to the components and can't be used outside
import { useState } from "react";
function MyComponent() {
// ok
const [count, setCount] = useState(0)
}
// Invalid hook call.
// Hooks can only be called inside of the body of a function component.
const [globalCount, setGlobalCount] = useState(0);
On SolidJS Signals have nothing to do with componentes, they are an independent tool and can be used without components at all.
On SolidJS components are just a tool to organize code.
import { createSignal } from "solid-js";
function MyComponent() {
// ok
const [count, setCount] = createSignal(0);
}
// ok
const [globalCount, setGlobalCount] = createSignal(0);
Components run only once
On React a function component runs every time something changes. This made React team feel the need to create a hook useEffect()
which is, by difference, the most complex concept and hardest to understand in React. It's primary responsibility is to "run code some times instead of every time the component is executed".
import { useEffect, useState } from "react";
import { createRoot } from "react-dom/client";
// we can put these values directly on useEffect's call
// but there they loose their meaning
// we create variables so what they do is clearly understood
const EXECUTE_ONCE = [];
const EXECUTE_ALWAYS = null;
const ONE_SECOND = 1000;
function MyComponent() {
const [count, setCount] = useState(0);
console.log("Rendering...", count);
useEffect(() => {
// setInterval wouldn't work here for reasons
// that escape the scope of this post
// this serves as an example of how complex useEffect is
setTimeout(() => setCount(count + 1), ONE_SECOND)
}, EXECUTE_ALWAYS)
return <div>{count}</div>
}
const root = createRoot(document.getElementById('root'));
root.render(<MyComponent />);
While in SolidJS a component is ran only once, any change in Signals will only affect the part of the HTML (or code) that use such Signal
import { createSignal } from "solid-js";
const ONE_SECOND = 1000;
function MyComponent() {
const [count, setCount] = createSignal(0);
console.log("Rendering once");
setInterval(() => setCount(count() + 1), ONE_SECOND);
return <div>{count()}</div>;
}
document.body.appendChild(<MyComponent />);
Here we can also appreciate the difference between SolidJS and ReactJS, the latter needs to pass through the Virtual DOM before reaching the real DOM so it needs us to import and use more abstractions:
import { createRoot } from "react-dom/client";
// ... and later...
createRoot(document.getElementById('root')).render(...)
In SolidJS <MyComponent />
returns a real DOM object that we can just append to the body
directly (never use body
with ReactDOM.createRoot()
or bad things will happen).
I've seen people present the fact that components run only once as something negative as "thinking each state of the application from zero" is one of React's mottos. But, in my opinion, that was never true due to the existence of useEffect()
. It doesn't matter what component you're looking at, if it has useEffect()
then we need to take into account previous and following executions.
JSX
There are also several details in the differences of JSX between ReactJS and SolidJS like how we set a CSS class to an element:
- ReactJS:
<div className="a" />
- SolidJS:
<div class="a" />
- HTML:
<div class="a"></div>
Or getting the real DOM object in React
import { createRef } from 'react';
function MyComponent() {
const ref = createRef();
return <div ref={ref} onClick={() => console.log(ref.current)} />
}
Vs getting the DOM object in Solid:
const ref = <div />
Or
function MyComponent() {
let ref;
return <div ref={ref} onClick={() => console.log(ref)} />
}
On top of that Solid comes with several utility components like
<Show when={...}>
<For each={...}>
<Dynamic as="div" />
- and more
Which in React have to be implemented by hand or from code, creating a mix of JSX and JS which may be difficult to follow.
Directives
Finally and more importantly, this is a feature that I simply haven't seen in React (because it wouln't fit anyway): extend elements with code.
Usually in React when we create a component that generates DOM element, for example a <div>
, we have to ensure our component accepts all properties that <div>
accepts, extract the ones that are relevant to our component and pass the rest to the div. This is easy in Javascript but when we use Typescript it gets a bit complicated:
Note: if you're using Javascript without Typescript... go try it.
// If we don't exclude the properties we're about
// to override Typescript will throw an error
type DivProps = Omit<
ComponentProps<"div">,
"onDrag" | "onDragStart" | "onDragEnd"
>;
type DraggableProps = DivProps & {
onDrag: (event: MyCustomEvent) => void;
onDragStart: (event: MyCustomEvent) => void;
onDragEnd: (event: MyCustomEvent) => void;
};
function Draggable({
onDrag,
onDragStart,
onDragEnd,
children,
...divProps
}: DraggableProps) {
return (
<div
onDrag={handleDrag}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
// just `draggable` doesn't work
draggable={true}
{...divProps}
>
{children}
</div>
);
function handleDrag() { /* ... */ }
function handleDragStart() { /* ... */ }
function handleDragEnd() { /* ... */ }
}
It looks like a lot of boilerplate, repetitive and doesn't provide much information.
In Solid this is addressed with directives, functions that aren't components by themselves but they run over a component.
Directives extend the DOM instead of containing it.
import { onCleanup } from "solid-js";
function draggable(el, { onDrag, onDragStart, onDragEnd }) {
el.setAttribute('draggable', 'true')
el.addEventListener('drag', handleDrag);
el.addEventListener('dragstart', handleDragStart);
el.addEventListener('dragstop', handleDragEnd);
onCleanup(() => {
el.removeAttribute('draggable')
el.removeEventListener('drag', handleDrag);
el.removeEventListener('dragstart', handleDragStart);
el.removeEventListener('dragstop', handleDragEnd);
});
function handleDrag() { /* ... */ }
function handleDragStart() { /* ... */ }
function handleDragEnd() { /* ... */ }
}
const element = <div use:draggable={{
onDrag() { /* ... */ },
onDragStart() { /* ... */ },
onDragEnd() { /* ... */ },
}} />;
Drawbacks
There are two points particularly painful when comparing Solid with React:
1. DO NOT DESTRUCTURE PROPERTIES
The props
object in SolidJS is a Proxy
, if you don't know what it means it's enough to be aware that Solid knows when you accesss props.something
and will return the updated value if it has changed.
If we use destructuring of props
we're reading all properties once when the component is created and if any of them was a Signal we won't get any updates:
import { createSignal } from "solid-js";
// DON'T DO THIS IN SOLID
function MyComponent({ a, b }) {
return <div>{a} - {b}</div>;
}
const [signal1, setSignal1] = createSignal();
const [signal2, setSignal2] = createSignal();
const element = <MyComponent a={signal1()} b={signal2()} />;
// <div> won't be updated
setSignal1('Hello')
setSignal2('World')
Instead of that we should take props
as a single variable and access it's properties wherever we use them:
import { createSignal } from "solid-js";
function MyComponent(props) {
return <div>{props.a} - {props.b}</div>;
}
const [signal1, setSignal1] = createSignal();
const [signal2, setSignal2] = createSignal();
const element = <MyComponent a={signal1()} b={signal2()} />;
// <div> will update once
setSignal1('Hello')
// <div> will update twice
setSignal2('World')
// or
import { batch } from 'solid-js'
// <div> will only be updated once
batch(() => {
setSignal1('Hello')
setSignal2('World')
})
2. Props manipulation
Following the previous point, this means that in Solid we can't just manipulate props
const { a, b, ...rest } = props
// or
const newProps = { ...defaultProps, ...props };
Solid instead provides two functions to do such operations: splitProps()
and mergeProps()
.
const [vowels, consonants, leftovers] = splitProps(
props,
["a", "e"],
["b", "c", "d"]
);
// and
const newProps = mergeProps(defaultProps, props);
Conclusion
In summary SolidJS is a library that has learnt a lot from React and gives us a similar development experience but removing all the abstraction layers that React (necessarily) addded when the web standards weren't mature enough for the applications we are developing.
I leave here my template to create projects with SolidJS, Typescript and Vite as compiler:
https://github.com/amatiasq/vite-solidjs-typescript-boilerplate
Have a great day