DEV Community

Jingtian Zhang
Jingtian Zhang

Posted on

SolidJS - An alternative to React?

What is Solidjs?

Today when I am surfing on internet searching for some highly performant frameworks for frontend, I found solidjs, which is a simple and performant reactive building user interfaces.

the project github link is here

Begin

while we can start using a vite-plugin-solid:

npx degit solidjs/templates/js my-solid-project
cd my-solid-project
npm install # or pnpm install or yarn install
npm run start # starts dev-server with hot-module-reloading
npm run build # builds to /dist
Enter fullscreen mode Exit fullscreen mode

then you can manually install vite related plugins :

# with npm
npm install -D vite vite-plugin-solid babel-preset-solid
npm install solid-js

# with pnpm
pnpm add -D vite vite-plugin-solid babel-preset-solid
pnpm add solid-js

# with yarn
yarn add -D vite vite-plugin-solid babel-preset-solid
yarn add solid-js
Enter fullscreen mode Exit fullscreen mode

after installing all those you will find in vite.config.js as

// vite.config.ts
import { defineConfig } from 'vite';
import solidPlugin from 'vite-plugin-solid';

export default defineConfig({
  plugins: [solidPlugin()],
});
Enter fullscreen mode Exit fullscreen mode

the code I use for some demo is here:


import logo from "./logo.svg";
import styles from "./App.module.css";

import {
  createEffect,
  createSignal,
  createMemo,
  createResource,
  Show,
  Switch,
  Match,
} from "solid-js";
import { render, Portal, Dynamic } from "solid-js/web";
const fetchUser = async (id) =>
  (await fetch(`https://swapi.dev/api/people/${id}/`)).json();

const User = () => {
  const [userId, setUserId] = createSignal();
  const [user] = createResource(userId, fetchUser);

  return (
    <>
      <input
        type="number"
        min="1"
        placeholder="Enter Numeric Id"
        onInput={(e) => setUserId(e.currentTarget.value)}
      />
      <span>{user.loading && "Loading..."}</span>
      <div>
        <pre>{JSON.stringify(user(), null, 2)}</pre>
      </div>
    </>
  );
};

function fibonacci(n) {
  if (n <= 1) return 1;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

function Modal() {
  return (
    <div class="container">
      <Portal>
        <div class="popup">
          <h1>Popup</h1>
          <p>Some text you might need for something or other.</p>
        </div>
      </Portal>{" "}
    </div>
  );
}

function Compare() {
  const [x] = createSignal(7);

  return (
    <Switch fallback={<p>{x()} is between 5 and 10</p>}>
      <Match when={x() > 10}>
        <p>{x()} is greater than 10</p>
      </Match>
      <Match when={5 > x()}>
        <p>{x()} is less than 5</p>
      </Match>
    </Switch>
  );
}

function Cats() {
  // <For each={}> to handle for loop, it updates or moves DOM rather than recreating them
  const [cats, setCats] = createSignal([
    { id: "J---aiyznGQ", name: "Keyboard Cat" },
    { id: "z_AbfPXTKms", name: "Maru" },
    { id: "OUtn3pvWmpg", name: "Henri The Existential Cat" },
  ]);
  return (
    <For each={cats()}>
      {(cat, i) => (
        <li>
          <a
            target="_blank"
            href={`https://www.youtube.com/watch?v=${cat.id}`}
            style={{ color: "white" }}
          >
            {i() + 1}: {cat.name}
          </a>
        </li>
      )}
    </For>
  );
}
// wrong naming would not work here
function Btn() {
  const [loggedIn, setLoggedIn] = createSignal(true);
  const toggle = () => setLoggedIn(!loggedIn());

  return (
    <Show when={loggedIn()} fallback={<button onClick={toggle}>Log in</button>}>
      <button onClick={toggle}>Log out</button>
    </Show>
  );
}

function Counter() {
  const [count, setCount] = createSignal(0);
  const fib = createMemo(() => fibonacci(count()));
  const doubleCount = () => count() * 2;
  createEffect(() => {
    console.log("the count is: ", count());
  });
  return (
    <>
      <button onClick={() => setCount(count() + 1)}>Click Me</button>
      {count()} <br />
      {doubleCount()} <br />
      fib: {fib()}
    </>
  );
}

function App() {
  return (
    <div class={styles.App}>
      <p>Counter demos how signal and createEffect / createMemo work</p>
      <Counter /> <p>Btn demos how simple conditional works in Solid</p>
      <Btn />
      <p>Cats demos how list rendering works using For tag </p>
      <Cats />
      <p>Compare demos how switch and match work</p>
      <Compare />
      <p>Modal demos how Portal works </p>
      <Modal />
      <p>Async demos </p>
      <User />
    </div>
  );
}

export default App;

Enter fullscreen mode Exit fullscreen mode

let's take a look one by one, this article will update

Entry and Components

  • one thing I like solidjs is that class is class instead of className, and the most of others are the same to react jsx writing
  • solidjs uses render() function to render, takes 2 args, first one is a function, second one is a container

Signal and Effect / Memo

signals are the most basic reactive primitive in solidjs, they track a single value, which can be any JS object that changes over time.

the createSignal function returns a pair of functions as a two-element array: a getter and a setter, basically remind me of useState

here we define a Counter, we define count as a signal, and two derived signals from it, that is fib and doubleCount

let's show how reactivity works in solidjs here, clicking the button to add number, and both doubleCount and fib will update

function Counter() {
  const [count, setCount] = createSignal(0);
  const fib = createMemo(() => fibonacci(count()));
  const doubleCount = () => count() * 2;
  createEffect(() => {
    console.log("the count is: ", count());
  });
  return (
    <>
      <button onClick={() => setCount(count() + 1)}>Click Me</button>
      {count()} <br />
      {doubleCount()} <br />
      fib: {fib()}
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

createEffect in solidjs is used to create a effect as a function

the effect automatically subscribes to any signal that is read during the function's execution and returns when any of them changes --> kinda like Vue's watch

Flow, Show

JSX allows you to use JS to control the logic flow in the templates, but when virtual DOM is not in use here, use of Array.prototype.map would wastefully recreate all the DOM nodes on every update

for a simple conditional display, solidjs provides a Show tag:

<Show
  when={loggedIn()}
  fallback={<button onClick={toggle}>Log in</button>}
>
  <button onClick={toggle}>Log out</button>
</Show>
Enter fullscreen mode Exit fullscreen mode

fallback prop acts as else and show when the condition passed to when is not true

example here:

// wrong naming would not work here
function Btn() {
  const [loggedIn, setLoggedIn] = createSignal(true);
  const toggle = () => setLoggedIn(!loggedIn());

  return (
    <Show when={loggedIn()} fallback={<button onClick={toggle}>Log in</button>}>
      <button onClick={toggle}>Log out</button>
    </Show>
  );
}
Enter fullscreen mode Exit fullscreen mode

For, Index, Switch, Match

coming to more complex conditionals, we need its provided

to render a list, due to the reason I mention above (no virtual DOM here), solidjs also provides For tag here

in this example we will render a list of index with its content

function Cats() {
  // <For each={}> to handle for loop, it updates or moves DOM rather than recreating them
  const [cats, setCats] = createSignal([
    { id: "J---aiyznGQ", name: "Keyboard Cat" },
    { id: "z_AbfPXTKms", name: "Maru" },
    { id: "OUtn3pvWmpg", name: "Henri The Existential Cat" },
  ]);
  return (
    <For each={cats()}>
      {(cat, i) => (
        <li>
          <a
            target="_blank"
            href={`https://www.youtube.com/watch?v=${cat.id}`}
            style={{ color: "white" }}
          >
            {i() + 1}: {cat.name}
          </a>
        </li>
      )}
    </For>
  );
}
Enter fullscreen mode Exit fullscreen mode

Switch and Match are used when we have to deal with conditionals with more than 2 mutual exclusive outcomes:

function Compare() {
  const [x] = createSignal(7);

  return (
    <Switch fallback={<p>{x()} is between 5 and 10</p>}>
      <Match when={x() > 10}>
        <p>{x()} is greater than 10</p>
      </Match>
      <Match when={5 > x()}>
        <p>{x()} is less than 5</p>
      </Match>
    </Switch>
  );
}
Enter fullscreen mode Exit fullscreen mode

well if you want to write tight and clean code, we can use Dynamic to write

<Switch fallback={<BlueThing />}>
  <Match when={selected() === 'red'}><RedThing /></Match>
  <Match when={selected() === 'green'}><GreenThing /></Match>
</Switch>

# to this 

<Dynamic component={options[selected()]} />
Enter fullscreen mode Exit fullscreen mode

ErrorBoundary, Suspense

well you can capture js error in the UI and fallback to what you want to display, first, define a <Broken />:

const Broken = (props) => {
  throw new Error("Oh No");
  return <>Never Getting Here</>
}
Enter fullscreen mode Exit fullscreen mode

and then use it:

  <div>Before</div>
      <ErrorBoundary fallback={err => err}>
        <Broken />
      </ErrorBoundary>
      <div>After</div>
Enter fullscreen mode Exit fullscreen mode

Image description

Lifecycle (onMount, onCleanup, onError)

solidjs has few lifecycle APIs here

onMount(async () => {
  const res = await fetch(`https://jsonplaceholder.typicode.com/photos?_limit=20`);
  setPhotos(await res.json());
});

const timer = setInterval(() => setCount(count() + 1), 1000);
onCleanup(() => clearInterval(timer));
Enter fullscreen mode Exit fullscreen mode

Conclusion

well, solidjs looks interesting, but it seems like Vue3 can do what it sells too, with much richer ecology and other stuff. I personally still stick to Vue.

Top comments (0)