{ Adrian.Matías Quezada }

SolidJS

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() and div.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 />);

View in codesandbox.io

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);

View in codesandbox.io

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 />);

View in codesandbox.io

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 />);

View in codesandbox.io

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);

View in codesandbox.io

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);

View in codesandbox.io

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 />);

View in codesandbox.io

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 />);

View in codesandbox.io

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 bodywith 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:

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

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() { /* ... */ },
}} />;

View in codesandbox.io

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