Discover A New Framework: SolidJS πŸ§‘β€πŸ’»

Discover A New Framework: SolidJS πŸ§‘β€πŸ’»

September 2nd, 2022 | 13 mins read

Published on dev.to and medium.com

The world of Web development is constantly evolving, and new JavaScript libraries are appearing (always more interesting), day after day...

This year (once again), I challenged myself to experiment with a new technology. As the Web is constantly evolving, it's important to get up to date every 6 months. Following the last "State Of JS" survey's results, I found two new frameworks that seemed promising to me: AlpineJS x SolidJS

NB: I'm passionate, I thirst for knowledge, and recently I wanted to learn something fresh, like the discovery of Svelte in 2019.

After reading two or three documentations, and analyzing several dozen lines of code, I preferred to choose SolidJS, especially because of the JSX omnipresence, as well as concepts similar to my everyday tool, namely: React!

In this post, I'll obviously talk about what SolidJS is, but especially about how I understood this new framework in a learning process (from theory to practice). I wouldn't fail to mention the dependencies that revolve around its ecosystem. Finally, we will see if this last one keeps its promise of "fine-grained reactivity", while being simple and efficient.

WTF is SolidJS!?

NB: Before talking about the main topic, a short explanation... As part of the work on setting up a peer-to-peer network in JavaScript (upcoming post...), I wanted to have an interface / an application to control actions related to a node (other than Postman). Rather than developing this from already known basics, I started to learn more in detail what SolidJS really is, and to implement it in a large-scale project. Here is my learning process!

Like many JavaScript libraries before it, SolidJS is a component-oriented framework for building Web interfaces. Although close to React in some of its concepts (and its syntax), it wants to be lighter and more reactive than this last one, like Svelte. Indeed, SolidJS advocates "fine-grained reactivity" by making changes directly from the DOM, rather than through a virtual DOM.

As a result (and according to performance tests of the open-source community), this new tool would be both faster than React, Vue, Preact (even if it has a lighter virtual DOM than React), as well as Svelte, and would be the first in terms of speed and memory allocation!

ReactVuePreactSvelteSolidJS
Create Rows1.33ms1.19ms1.20ms1.26ms1.05ms
Create Many Rows1.74ms1.27ms1.31ms1.26ms1.14ms
Partial Update1.37ms1.20ms1.20ms1.11ms1.04ms

Create 1 000 / 10 000 items, and modify 100 items (milliseconds)

ReactVuePreactSvelteSolidJS
Ready Memory1.20MB1.14MB1.04MB1.01MB1.02MB
Run Memory3.22MB2.42MB2.76MB1.71MB1.57MB
Run Memory (Update)3.62MB2.41MB2.76MB1.67MB1.58MB

Memory allocation for loading, creation and modification (megabytes)

Get started with SolidJS

Getting started with SolidJS is child's play (I still recommend having the basics of JavaScript, as well as the mastery of some component-oriented framework notions, to fully understand what reactivity is...). By using the Rich Harris's library (degit), we can scaffold the project. Therefore, the JavaScript environment will be pre-configured to work from a bundler, not Webpack this time, but Vite!

Powered mainly by the Vue community, Vite is a new kind of JavaScript tool, to start a new Web project very quickly, while offering a modern environment. Inspired by RollupJS (but also ESBuild), Vite has many features, including:

  • NPM dependency management
  • Pre-bundle of sources (saving a lot of time at startup)
  • Hot Module Replacement (HMR)
  • Native TypeScript support
  • On-the-fly JSX transpilation
  • Support for CSS modules
  • Dynamic import management
  • Build optimization, etc.

NB: Vite ecosystem also brings Vitest for unit testing, as an alternative to Jest.

SolidJS offers a wide range of templates to initialize a new project with, the choice of language: JavaScript or TypeScript; the integration of a ready-to-use UI library (including the great TailwindCSS), but also the implementation of an unit test engine: Jest / Vitest (optional).

import { render } from 'solid-js/web';
import { createSignal, onMount, onCleanup } from 'solid-js';

const CountingComponent = () => {
  const [count, setCount] = createSignal(0);
  const interval = setInterval(() => {
    setCount(count => count + 1);
  }, 1000);

  onMount(() => console.log('Mounted'));
  onCleanup(() => clearInterval(interval));

  return <div>Count value is {count()}</div>;
};

render(() => <CountingComponent />, document.getElementById('root'));

Counter component with SolidJS

import { render } from 'react-dom';
import { useState, useEffect } from 'react';

const CountingComponent = () => {
  const [count, setCount] = useState(0);
  const interval = setInterval(() => {
    setCount(count => count + 1);
  }, 1000);

  useEffect(() => {
    console.log('Mounted');
    return (cleanup = () => clearInterval(interval));
  }, []);

  return <div>Count value is {count}</div>;
};

render(() => <CountingComponent />, document.getElementById('root'));

Counter component with React

Above, here's what a first single counter component might look like with SolidJS, and its opposite on the React side. We quickly notice the similarities between these two libraries. And yet, SolidJS seems to be more explicit and simple!

Indeed, rather than having a useEffect hook to handle the mounting and unmounting behaviors of the component (through a cleanup function), SolidJS exposes its actions with functions of its life cycle: onMount and onCleanup.

Similarly, the createSignal API is relatively identical to React's useState hook in how its works, since the data flow is unidirectional. On one side the accessor (immutable), and on the other the "setter". However, there is a significant difference with SolidJS! The "fine-grained reactivity" concept is based on the principle that components only renders once, and that functions associated with the Signals are executed as soon as they're updated. Changes will then be impacted directly in the DOM. Since the access to the value of a Signal is only possible by calling it (presence of parentheses), I consider the accessor more like a "getter", rather than a constant... Signals are the key concept of SolidJS!

Overall SolidJS is really easy to take in hand, learning its API is also easier due to rich documentation and a "playground" section to understand each aspect of this library, and to gain in skill step by step.

Although basic, SolidJS is a complete framework, since it also offers a routing solution (npm install solid-app-router), a "code splitting" feature through asynchronous component loading (made possible by the bundler), or even the ability to render your application server-side (SSR). So SolidJS, ranks among the big ones (React, Vue, etc.) as a Web library to create user interfaces.

Building user interfaces

Anatomy Of Modal

Enough theory, now let's move on to practice! And what better way to put it into practice than to evolve in a concrete case… To do this, I suggest you to create a new project, to handle the feeding and display of a modal, in order to approach the reactivity notions of SolidJS. Let's go!

npx degit solidjs/templates/js-vitest my-awesome-app
cd my-awesome-app
npm install

Et voilΓ ! You should see a new directory appear (my-awesome-app in my case), lite in terms of dependencies, but already pre-configured to work /vit/!

NB: As part of my work, I also added TailwindCSS support to project initialization (npx degit solidjs/templates/js-tailwindcss), then merged both Vite configurations manually.

import { mergeProps, Switch, Match, Show } from 'solid-js';
import { IconInfo, IconSuccess, IconWarning, IconError } from './icons';

export const Modal = (props) => {
  const merged = mergeProps({
    type: 'INFO',
    title: 'Info',
    onOk: () => console.log('Ok')
  }, props);

  return (
    <div class="container">
      <div class="modal__background" />

      <div class="modal">
        <span class="modal__icon">
          <Switch fallback={<IconInfo class="text-blue-700 bg-blue-100" />}>
            <Match when={merged.type === 'SUCCESS'}>
              <IconSuccess class="text-green-700 bg-green-100" />
            </Match>

            <Match when={merged.type === 'WARNING'}>
              <IconWarning class="text-orange-700 bg-orange-100" />
            </Match>

            <Match when={merged.type === 'ERROR'}>
              <IconError class="text-red-700 bg-red-100" />
            </Match>
          </Switch>
        <span>

        <span class="modal__title">{merged.title}</span>

        {props.children}

        <div class="modal__actions">
          <Show when={typeof props.onCancel === 'function'}>
            <button class="modal__action-cancel" type="button" onClick={props.onCancel}>
              Cancel
            </button>
          </Show>

          <button class="modal__action-ok" type="button" onClick={merged.onOk}>
            Ok
          </button>
        </div>
      </div>
    </div>
  );
}

Modal.jsx

The modal represents a stateless component that visually extends across the screen (considering the dark background and centering of the main element), and which only waits for props values to come to life. This also simplify unit testing with Vitest (and Testing Library).

The SolidJS API can make the syntax a bit verbose at first "Vue" πŸ˜‰, but its components are essential to effect a change in the DOM, because of a virtual DOM lack. So its simplicity lies in a clever combination of Signals (immutable) and components (<Show>, <Switch>, <For>, etc.) in order to make the DOM reactive to the right places!

NB: Note that here, we use the mergeProps API to impose default props values. Again, it's important to use this function to conserve reactivity, rather than initializing these same values directly from the component's signature (like with React).

import { render } from 'solid-js/web';
import { createSignal, onMount, Show } from 'solid-js';
import { Modal } from './Modal';

export const App = () => {
  const [showModal, setShowModal] = createSignal(false);
  const [modalType, setModalType] = createSignal('');
  const [modalTitle, setModalTitle] = createSignal('Lorem ipsum');

  onMount(() => {
    if (showModal()) {
      setTimeout(() => {
        setModalType('SUCCESS');
        setModalTitle('Everything is good');
      }, 1500);

      setTimeout(() => {
        setModalType('ERROR');
        setModalTitle('Something is wrong');
      }, 3000);
    }
  });

  return (
    <div class="app">
      <Show when={showModal()}>
        <Modal
          type={modalType()}
          title={modalTitle()}
          onOk={() => {
            setModalType('WARNING');
            setShowModal(false);
          }}>
          <p>Lorem ipsum dolor sit amet</p>
        </Modal>
      </Show>

      <button
        style={{ margin: 'auto' }}
        onClick={() => setShowModal(currentVal => !currentVal)}>
        {showModal() ? 'Hide Modal' : 'Show Modal'}
      </button>
    </div>
  );
};

render(() => <App />, document.getElementById('app'));

main.jsx

In terms of usage, the triggering of the modal's display (as well as its feeding) takes place via Signals, from the parent component. Above, I had fun changing the modal's appearance over time, to observe the reactivity. One more time, it may seem verbose, but it's simple and effective!

This mode of operation is sometimes not enough, especially when developing an application at scale, or when using a routing system (npm install solid-app-router), to split the pages of its application. Handling the modal should be possible by using a "props drilling" logic (parent to child, child to sub-child, etc.), but we should quickly fall into a complexity linked to component nesting, as well as their maintenance... No problem! As for modals, there is the <Portal> component; for everything else, there is the "Context" API!

import { mergeProps, Switch, Match, Show, Portal } from 'solid-js';
import { IconInfo, IconSuccess, IconWarning, IconError } from './icons';

export const Modal = props => (
  <Portal mount={document.getElementById('modal')}>
    <ModalComponent {...props} />
  </Portal>
);

// Move <Modal /> To <ModalComponent />
export const ModalComponent = props => {
  const merged = mergeProps(
    {
      type: 'INFO',
      title: 'Info',
      onOk: () => console.log('Ok')
    },
    props
  );

  return <div class="container">{/* ... */}</div>;
};

Modal.jsx

Context API

NB: In a previous post, I explain how it's possible to implement a "homemade" Redux, with React's Context API, here the logic is the same...

import { createContext, createEffect, createSignal, useContext } from 'solid-js';
import { createStore } from 'solid-js/store';

const store = createContext();

const { Provider } = store;

export const useStore = () => {
  return useContext(store);
};

const StateProvider = props => {
  const [currentIdx, setCurrentIdx] = createSignal(0);
  const [state, setState] = createStore({
    email: 'morty.smith@pm.me'
  });

  createEffect(() => {
    console.log('currentIdx', currentIdx());
  });

  const value = [
    {
      currentIdx,
      fieldValues: state
    },
    {
      incrementIndex() {
        setCurrentIdx(prevCurrentIdx => prevCurrentIdx + 1);
      },
      decrementIndex() {
        setCurrentIdx(prevCurrentIdx => prevCurrentIdx - 1);
      },
      getField(key) {
        return state[key] || '';
      },
      setField(key, value) {
        setState({ [key]: value });
      }
    }
  ];

  return <Provider value={value}>{props.children}</Provider>;
};

export default StateProvider;

StateProvider.jsx

import { Form, InputField } from './components';
import { useStore } from './StateProvider';

const StateConsumer = () => {
  const [{ currentIdx }, { getField, setField }] = useStore();

  return (
    <Form index={currentIdx()}>
      <InputField
        label="Email"
        type="email"
        placeholder="rick.sanchez@pm.me"
        defaultValue={getField('email')}
        handleChange={e => setField('email', e.target.value)}
      />
    </Form>
  );
};

StateConsumer.jsx

State Management (via the Context API), is an ideal reactive programming paradigm for large developments, avoiding the problem of "props drilling", but also the presence of double / triple data... In this way, it's said that the "store" is the only source of truth of the application.

NB: To learn more about this topic, don't hesitate to consult the source code of my project on Gitlab and Vercel.

Would you like some extra unit tests?

The ecosystem powered by SolidJS, advocates modern technologies such as PNPM, Vite, Vitest but also Testing Library. Based on the implementation of Preact, the SolidJS team brings the component-oriented unit testing framework, which has already proven itself with React, Vue, Angular, Cypress, etc.

import { render, fireEvent } from 'solid-testing-library';
import { describe, it, expect, vi } from 'vitest';
import { Modal } from './Modal';

describe('<Modal />', () => {
  it('Should Render', () => {
    const { getByText, unmount } = render(() => (
      <Modal title="> Modal Title Goes Here <">
        <p>Lorem ipsum dolor sit amet</p>
      </Modal>
    ));

    expect(getByText('> Modal Title Goes Here <')).toBeInTheDocument();
    expect(getByText('Lorem ipsum dolor sit amet')).toBeInTheDocument();
    unmount();
  });

  it('Should Trigger onClick Event(s)', async () => {
    const mockedOnCancel = vi.fn();
    const mockedOnOk = vi.fn();

    const { getByText, unmount } = render(() => (
      <Modal
        type="WARNING"
        title="Warning"
        onCancel={mockedOnCancel}
        onOk={mockedOnOk}
      />
    ));

    const cancelBtn = getByText('Cancel');
    fireEvent.click(cancelBtn);
    expect(mockedOnCancel).toHaveBeenCalled();

    const okBtn = getByText('Ok');
    fireEvent.click(okBtn);
    expect(mockedOnOk).toHaveBeenCalled();

    unmount();
  });
});

Modal.test.jsx

Simplicity + Reactivity === Performance

I really enjoyed playing with this new framework (as often with JavaScript technologies). There are many notions of its counterparts, as well as new ones such as Signals, or the createResource function (very useful for querying APIs). I didn't have great difficulty creating my node control application (from scratch), since this last one offers great flexibility in project organization. So, the Developer eXperience is optimal!

NB: Unlike React (or Vue), SolidJS compiles its code during the development period (like Svelte). In this way, it's able to effectively and quickly effect changes in the "real" DOM; one bonus point in favor of the DX! πŸ‘

This first overview makes me want to know more and propose the approach in professional situation. Perhaps replacing Preact, or even React... SolidJS doesn't offer only components and features demonstrated above. If I had to make a quick list, this library also offers:

  • Dynamic rendering of an element list, from (not Array.prototype.map but) its <For> component
  • Encapsulation of asynchronous calls (as resources), for better visibility of the loading state, as well as the presence (or not) of error(s)
  • Interface display in case of lazy loading (components or resources), via the <Suspense> component
  • Display of interfaces in case of error(s), to avoid application crash
  • The SSR operating mode, for performance gains, but also for better SEO (here SolidJS, directly confronts Nuxt, Next or SvelteKit)
  • Or, the "store" reconciliation, allowing Redux to coexist in a scaled application

Due to its easy-to-understand concepts, and despite the absence of a virtual DOM, SolidJS has proven its strength and stability (in 5 years of development), pushing reactivity in its purest form: Signals! Just like Svelte, this future-oriented framework has bright days ahead of it. This is my favorite of the year! ❀️