Solid JS Explained

Watch the video on YouTube

Let’s start by reviewing this basic component, which will give you an overview of all the cool things Solid has to offer. Then we’ll go through the architectural decisions and implementation details used to achieve such powerful reactive and rendering mechanisms. Finally, in the second part of the article we’ll build a small app where you’ll see Solid’s seamless dev experience in action.

Ok, so all modern frameworks have three big things in common.

  1. First, they have some sort of mechanism that allows you to group code together. Most of the time this is done via class or function Components.

  2. Second, your app will work with data. Frameworks need to monitor this data and react whenever data changes occur.

  3. Finally, this data and the associated changes have to be displayed in the DOM via some sort of templating mechanism.

Signals & JSX

Solid follows the most popular route by employing function components to group code, signals to achieve reactivity and JSX as the templating system. It is important for you to understand that the function is completely decoupled from the Solid internals. In other words, the function code will be executed only once, when Solid first evaluates the signals, events and lifecycle hooks it needs to manage. Then lines like these ones will not be executed ever again.

export function PasswordReset() {
  console.log("=> executing the App");

  const [pass, setPass] = createSignal("");
  const length = () => pass().length;
  const [valid, setValid] = createSignal(false);

  createEffect(() => {
    setValid(
      length() > 8 &&
      pass() !== pass().toLowerCase()
    );
  });

  return (
    <form>
      <input value={pass()} onChange={ev => setPass(ev.target.value)} />
    </form>
    <button disabled={!valid()}>Save</button>
  );
}

Signals are a well established software pattern used to achieve reactivity in a granular manner. Using signals on the web is not a novel concept, and they were first made popular in the frontend space by Knockout JS back in 2010. The main idea behind signals is that they can hold values while also maintaining a list of dependencies. Associated computed signals and effects will silently register themselves in this dependency array and, whenever the value inside the signal will be updated these dependencies will rerun.

The effects can either be explicitly defined by you, or implicitly defined by Solid whenever a signal value is used in the JSX template.

And since I mentioned the template, JSX is the most established templating language in frontend development. It was introduced by React, and adopted by the likes of Solid, Qwik and even Vue thanks to its simplicity and expressiveness. Of course, here you can use all the expected tools to build UIs ranging from registering event handlers to rendering list of elements.

Ok, with the basics out of the way, let’s quickly review the 5 Solid core features you should really be aware of.

Truly Reactive

First of all, as I already mentioned, Solid is built with Reactive Primitives at its core. Thanks to signals and a fine-grained reactivity model, the framework can track changes at a very granular level. The result is an efficient update process that doesn’t rely on dirty checking virtual DOM snapshots.

So thanks to this architecture, Solid doesn’t require a Virtual DOM. As a result we gain a more efficient rendering and better performance.

Fast

With no unnecessary dependencies, and a highly optimized core implementation it is no wonder Solid is both small and fast. These are key aspects in this day and age where speed is everything.

Compiled & Small

Most of these benefits are achieved thanks to a compilation step. Solid JS is a compiler-based framework, which means it does much of its work at compile-time rather than runtime. This approach results in smaller bundle sizes and faster runtime performance, as less JavaScript needs to be shipped and executed in the browser.

Complete

Finally, as you’ll see in a second, Solid provides a whole collection of modern framework features like Context, Portals, Suspense, streaming SSR, progressive hydration, Error Boundaries and concurrent rendering.

Solid is built on top of Vite, so let’s run the init command with the TypeScript template to initialize a project.

npm create vite@latest my-app-template solid-ts

The Context is the most common way to share state between components, but note that Solid also offers a native state management implementation to address more complex scenarios.

function App() {
  const [logged, setLogged] = createSignal(false);

  const context = [
    logged,
    {
      login: () => setLogged(true),
      logout: () => setLogged(false),
    },
  ];

  return (
    <AppContext.Provider value={context}>
      <Router>
        <Routes>
          <Route path="/" component={Dashboard} />
          <Route path="/auth" component={Auth} />
        </Routes>
      </Router>
    </AppContext.Provider>
  );
}

In the context I am adding our login state, and some helper methods to easily update the authentication status. Then, using the Solid Router I am defining a Dashboard and an Authentication Page route.

Next, let’s jump into the Dashboard page component, where we can grab the context via the “useContext” hook.

export default function Dashboard() {
  const [auth] = useContext(AppContext);

  const [hasAuthModal, setAuthModal] = createSignal(false);
  const [members, setMembers] = createSignal<Member[]>([]);

  onMount(async () => {
    setAuthModal(!auth());
    setMembers(await fetchMembers());
  });

  return (
    <>
      <h1>Dashboard</h1>

      <For each={members()} fallback={"No members yet."}>
        {member => <h3>{member.name}</h3>}
      </For>

      <Show when={hasAuthModal()}>
        <AuthModal onClose={() => setAuthModal(false)} />
      </Show>
    </>
  );
}

Solid also comes packed with lifecycle hooks, which are constructs allowing you to run code at specific points in the Component lifetime. When the component is mounted, I am displaying a modal if the user is not authenticated, and I am fetching a list of members from the server, which is then simply stored into a signal.

In the JSX area, I am using the special For component to iterate and render the list of members. Please note that in JSX it is fairly common to map object instances to DOM elements directly, but this is not going to properly work in Solid. Because it lacks a virtual DOM, the framework relies on the For construct to do the needed internal indexing and DOM update optimizations.

If the user is not logged in, we are showing a modal pointing to an authentication page. Two things of note here.

interface AuthModalProps {
  onClose: () => void;
}

export default function AuthModal({ onClose }: AuthModalProps) {
  return (
    <Portal>
      <div class="modal" use:clickOutside={onClose}>
        <h3>Login for the best experience</h3>
        <A href={"/auth"}>Login</A>
      </div>
    </Portal>
  );
}

First, we are using the Portal component, which will add the content of our component directly in the document.body. This is great when it comes to styling modal components.

Also, we are using a custom directive via the “use:” namespace. The custom directive is a function taking two arguments.

export default function clickOutside(el, accessor) {
  const handler = (ev) => {
    if (!el.contains(ev.target)) {
      accessor();
    }
  };

  document.body.addEventListener("click", handler);
  onCleanup(() => document.body.removeEventListener("click", handler));
}

Finally, in the login page, we are listening to the form submit event, performing the authentication on the server, and update the context implementation.

export default function Auth() {
  const [, { login }] = useContext(AppContext);

  const [mail, setMail] = createSignal("");
  const [pass, setPass] = createSignal("");

  async function onSubmit() {
    await performLogin(mail(), pass());

    login();
  }

  return (
    <form onSubmit={onSubmit}>
      <input value={mail()} onChange={(e) => setMail(e.target.value)} />
      <input value={pass()} onChange={(e) => setPass(e.target.value)} />
      <button>Submit</button>
    </form>
  );
}

Solid JS is a really powerful tool to have in your arsenal, and you can check out these videos to see how easy it is to integrate it in any modern tech stack.

Until next time, thank you for reading!