A React Developer’s First Take on Solid

When I start a new project, I tend to rely on tools I already know well. I have a vision in my head, and learning a new technology as I try to build something is a recipe for getting nothing done. Usually, for projects on the web, that means picking React.

That said, I do keep an eye on the JavaScript ecosystem as a whole, and one of the frameworks that has piqued my interest for a while is Solid. It’s a reactive JavaScript frameworks, which basically means it models your app as streams of data and automatically updates the UI when a dependency changes. That might sound abstract and weird, but we’ll unpack it in a bit.

I recently started a project involving server-side rendering. While looking into technologies to use,1 I discovered that the Solid team had recently released Solid Start — a Next.js-esque “meta-framework” that makes the experience of working with Solid easier and more seamless.

I haven’t gotten a chance to work with it in depth yet, but I have used a decent amount of what it has to offer. So far, I’m really enjoying it. It’s similar enough to React that there’s not a steep learning curve, and some of the features seem genuinely transformative.

There are two parts to this post. First, my impression of the Solid framework itself; and second, my impression of Solid Start.

So, without further ado: a React developer’s first take on Solid.

The Solid Framework

The creator of Solid, Ryan Carniato, once quoted someone saying “Svelte is to Vue as Solid is to React”. That rings true to me. It’s not “just JavaScript” in the same way React is — if you look at the compiled code, it’s definitely doing more than just de-sugaring JSX into function calls. But the code you’re writing is JavaScript, not a language that’s bespoke to the framework.

Here’s what a simple component might look like in Solid:

import { render } from "solid-js/web";
import { createSignal } from "solid-js";

function Greeter() {
  const [name, setName] = createSignal("Jake");

  const yelling = () => name().toUpperCase();

  return (
    <>
      <p class="greeting">Hello, {yelling()}!</p>
      <input
        value={name()}
        onChange={event => {
          setName(event.currentTarget.value);
        }}
      />
    </>
  );
}

render(() => <Greeter />, document.getElementById("app"));

You could be forgiven for thinking you’re looking at a React component. From a syntax point of view, the only difference at this point is the use of class instead of className.2

Under the hood, however, they’re very different. In React, a component function gets called every render, and React modifies the DOM based on the difference between the return values. In Solid, a component function gets called once over the entire lifetime of the component, and Solid only updates the DOM for each property that changes.

Let’s break apart the example. There are four relevant lines:

  1. const [name, setName] = createSignal("Jake"); looks like React’s useState, but with one key difference: name is not the value itself, but a function that returns the current value. So in this case, you could call name() and it would return "Jake".
  2. const yelling = () => name().toUpperCase(); is where it becomes important that name is a function, rather than a primitive value. Any time yelling is called, it gets the current value, not just the value when the component was run. Under the hood, Solid remembers that the return value of yelling depends on name.
  3. <p class="greeting">Hello, {yelling()}!</p> is the trickiest part, because JSX in Solid works slightly different than in React. Rather than just rendering this immediately, Solid remembers that the text of this <p> element depends on the result of the yelling function. Then, when that function’s dependencies change, Solid figures out what actually changed and updates the DOM again.
  4. setName(event.currentTarget.value); works roughly the same as in React. Calling it updates the value of the name signal, which causes Solid to re-evaluate anything that depends on it — in this case, the yelling function and the text inside the <p> tag.

Essentially, what the component does is set up a pipeline from createSignal to the DOM.3 When you call render, Solid remembers how that whole pipeline works. Then, when the value of the signal changes — in this case, by calling setName — Solid runs only that pipeline, not bothering with any other pipelines that it knows haven’t changed.

I know that’s kinda hard to visualize. If you’re interested, Ryan has a good talk that goes in-depth into how this works. In practical terms, though, it doesn’t have a huge impact on the developer experience. There are a couple gotchas to keep in mind, but for the most part you can write your code as if you were building a React app and it just… works.

Okay, so what are the gotchas? One, no destructuring props. Solid does some fancy Proxy stuff to track property access that gets messed up if you do.

// wrong
function Greeter({ name }) {
  return <p>Hello, {name}!</p>;
}

// right
function Greeter(props) {
  return <p>Hello, {props.name}!</p>;
}

Two, because component functions only run once, you need to wrap any derived values in their own functions. And three (or I guess two and a half) you use special components for looping and showing/hiding, rather than maps and ternaries.

// wrong
function TodoList(props) {
  const important = props.todos.map(todo => todo.toUpperCase());

  return (
    <ul>
      {important.map(todo => (
        <li>{todo}</li>
      ))}
    </ul>
  );
}

// right
function TodoList(props) {
  const important = () => props.todos.map(todo => todo.toUpperCase());

  return (
    <ul>
      <For each={important()}>{todo => <li>{todo}</li>}</For>
    </ul>
  );
}

Again, these are pretty small drawbacks. They’re kind of like React’s Rules of Hooks, in that you might initially think it’s weird to need to call functions in the same order each time until you realize that you already write most of your code like this anyway.

Let’s look at one more example — the infamous counter app:

function Counter() {
  const [count, setCount] = createSignal(0);

  console.log("component", count());
  createEffect(() => {
    console.log("effect", count());
  });

  return (
    <div>
      <Count count={count()} />
      <Button onIncrement={() => setCount(count => count + 1)} />
    </div>
  );
}

function Count(props) {
  return <p>{props.count}</p>;
}

function Button(props) {
  return (
    <button type="button" onClick={props.onIncrement}>
      Increment
    </button>
  );
}

Again, very similar to React. createSignal plays the role of useState, and the API is almost identical except that the "state" variable count is a function that returns the current value. You’ll also notice I put a console.log inside the Counter component — once in the body itself, and once in createEffect, which is Solid’s version of useEffect.

Solid knows that there’s a "pipeline" between that count variable and the <p> tag, it and updates the DOM automatically when the value changes. It also knows that there’s another pipeline that from count to createEffect, so it reruns the effect.

Here’s a link to a live version of this code, if it helps. If you run it and click the button a few times, you’ll see that the “component” log doesn’t happen when you click; these functions are, indeed, only called once. The "effect" log, on the other hand, happens every time the count is updated. You should see something like this in the console:

component 0
effect 0
effect 1
effect 2
effect 3

All this is nice, but what’s the advantage over React? For one, it’s small, although that makes less of a difference the larger your app gets. There are marginal developer experience improvements, such as not having to track createEffect dependencies. And finally: because it’s only updating the exact things that change, it’s also really fast.

Solid Start

If Solid is React, then Solid Start is Next.js. The Solid Start website cautions that it’s in beta, and they’re definitely not lying. But it also has some awesome features that seem genuinely transformative.

First, it has the file system routing that’s become bog standard amongst frontend frameworks these days. The default styling solution is CSS modules. It supports both server-side rendering and client-side rendering. There are provided components for managing head tags.

Let’s talk about loading data. Traditionally, single-page apps are split into two layers: a JavaScript frontend that runs in the browser, and a separate REST(ish) API that runs on the server, communicating with the client using JSON. Lately, frameworks like Next.js have begun combining the two, allowing developers to create “API routes” in the same codebase as their user-facing components.

Solid Start’s answer to this is route data — a Remix-inspired feature that makes it super easy to load data, both on the server and client. If you export a function called routeData from a page, calling the hook useRouteData within the component will give you the data you returned from routeData. The secret sauce is that Solid takes care of wiring up the HTTP request and getting the response back to your component:

export function routeData() {
  const [todos] = createResource(async () => {
    // here’s where you’d load data if it weren’t hardcoded as an example
    return ["brush teeth", "get milk", "publish blog post"];
  });

  return { todos };
}

export default function Page() {
  const { todos } = useRouteData();

  return (
    <ul>
      <For each={todos()}>{todo => <li>{todo.text}</li>}</For>
    </ul>
  );
}

That’s for reading data. For writing, there are route actions. They’re pretty similar, except you call them from within your component code and wire them up to your JSX, like a hook. They even return a <Form> component, which makes it super easy to create progressively enhanced HTML forms that work without JavaScript:

export default function Page() {
  const [name, { Form }] = createRouteAction(async data => {
    // here’s where you’d do some stuff with the data
    return data.get("name");
  });

  <>
    <Form>
      <label>
        Name
        <input name="name" />
      </label>
      <button>Submit</button>
    </Form>
    <p>Hello, {name.result}!</p>
  </>;
}

I won’t bury the lede: it’s pretty magical. Solid Start even includes functions createServerData$ and createServerAction$ for operations that should only happen on the server. You can make database queries in the same file as your components, and Solid will not only render the markup on the server when the page first loads, but also request new data and update the DOM whenever any of the parameters change. It’s way faster and less boilerplate-y than manually writing new REST endpoints or GraphQL resolvers.

Frankly, I’m a little skeptical of how well this scales. As apps grow, they tend to require additional complexity. Things like authorization and rate limiting. Although the Solid Start documentation does include an example of how to implement sessions, my gut says that bigger apps will still benefit from a separate “traditional” API. But for starting out quickly, or for apps that are likely to remain small, such as prototypes or side projects, this is perfect.

Solid Start does offer the API routes found in Next.js as well, though. In files within the routes directory, you can export functions named after HTTP verbs to respond to requests of that type. So, in case you do end up needing to scale your API beyond what route data and route actions can achieve, Solid Start gives you tools to migrate gradually.

All that said, Solid Start is definitely beta software. Some of the rough edges I’m hoping get fixed before the 1.0 release:

  • When rendering HTML on the server, Solid Start adds a bunch of weird data-hk attributes to all the elements.
  • Some naming conventions aren’t really clear, and the documentation doesn’t explain them. For example, some functions use $ as a suffix. My guess is that’s for functions that run on the server, but they all have "server" in the name anyway, so it seems redundant. Likewise, I’m not sure what the difference is between functions prefixed with use and create.
  • createServerData$ doesn’t accept a reactive function, and it’s not entirely clear why. Rather than automatically tracking dependencies such as query string parameters, you need to use them to construct a key which determines when to re-fetch the data. This overhead doesn’t exist on "normal" Solid functions like createEffect and createResource.
  • Possibly the most annoying thing is that data is inconsistently serialized when moving between the server and client. In my usage, it seems like objects in the initial page load are returned to the component as actual objects, but once you start taking actions on the page they become strings.4 It’s a major leak in the client/server abstraction. I’m not sure whether it’s a bug or simply an oversight, but I really hope it gets fixed.

Looking Forward

After all that, the question is: would I use Solid and/or Solid Start for a "real" project? I think the answer is "not yet".

I had a lot of fun building with it, and there are some really promising features. Despite having a lot of experience with React, building a full stack app with Solid took significantly less time and boilerplate. It’s a solid start.5

But right now, React really is a behemoth6 when it comes to usage. Solid is definitely my favorite React alternative that I’ve tried, but that doesn’t mean the community agrees.

Usage matters because the community is one of the most important metrics to me. Maybe the most important. That’s how a technology gets tested; bugs get found and fixed; plug-ins get created. It’s a bit of a chicken-and-egg situation, because the lack of those things is what prevents the community from forming in the first place.

So: for large projects, I’m sticking to React. But for side projects, prototypes and experiments? I’m definitely adding Solid to my toolbox.

Footnotes

  1. I find it difficult to choose a stack when it comes to “traditional” web apps. To me, structuring my UI code as components makes sense even if it doesn’t run on the client. And I’d much rather write each component in the programming language itself, rather than an entirely different templating language. Finally — and I don’t know how this hasn’t caught on outside of single-page app frameworks — but CSS modules are a requirement (I don’t like Tailwind). That’s pretty much the entire reason I don’t use Remix, even though it touts a “zero JavaScript by default” experience that checks all my other boxes: it doesn’t support CSS modules (yet).

  2. Call it Stockholm Syndrome, but I actually prefer className to class. I know class matches the HTML attribute, but className matches the JavaScript DOM property. Plus, class is a reserved word, so you can’t destructure it out of the props object — although this isn’t a problem in Solid, because you can’t destructure props at all.

  3. In reactive programming lingo, you can think of a signal as a source, and the DOM as a sink.

  4. I ran into the serialization issue while trying to return Date objects from routeData. Eventually I gave up and just returned strings that I parsed in the component on the client side.

  5. I’m sorry! I held out as long as I could!

  6. There are really weird spikes in the Svelte and Vue data that distort the graph. If you ignore those, you can see that React has over four times as many downloads as Vue and Angular, two orders of magnitude more than Svelte and three orders of magnitude more than Solid. NPM downloads are only a directionally accurate indicator of popularity, but you don’t need a statistics degree to see that Solid has some catching up to do.