Skip to main content

JSX Beyond React: An Intro to SolidJS

JSX makes building UIs easier, but not all frameworks handle it the same. Discover how React and SolidJS take different paths to rendering, state management, and performance.

If you’ve worked with React, you’re probably familiar with JSX, which is a way that HTML-like elements can be written inside of React components. However, JSX isn’t just for React anymore. JavaScript frameworks, like Solid, also use it to streamline UI development. While React and Solid share the same JSX syntax, they handle rendering and state updates very differently. Let’s take a look at how they compare.

JSX in React

JSX, short for JavaScript XML, is a JavaScript syntax extension (often called syntactic sugar) that allows developers to write HTML-like elements in JavaScript. JSX code is transpiled into plain JavaScript, which React uses to create its virtual DOM. On the first render, this will inform what the real DOM looks like.

Here’s a simple React example that renders a card with a dynamic title and content, using JSX:

import { useState } from "react";

function Card({ title, content }) {
  return (
    <div className="card">
      <h2>{title}</h2>
      <p>{content}</p>
    </div>
  );
}

function App() {
  const [title, setTitle] = useState("Hello");
  const [content, setContent] = useState("This is a React component.");

  return <Card title={title} content={content} />;
}

export default App;

These two components would be transpiled into plain JavaScript, which might look something like this:

function Card(props) {
  return React.createElement("div", { className: "card" },
    React.createElement("h2", null, props.title),
    React.createElement("p", null, props.content)
  );
}

function App() {
  const [title, setTitle] = useState("Hello");
  const [content, setContent] = useState("This is a React component.");

  return React.createElement(Card, { title: title, content: content });
}

export default App;

Then, our virtual DOM might look something like this:

{
  type: App,
  props: {},
  key: null,
  ref: null,
  children: {
    type: Card,
    props: {
      title: "Hello",
      content: "This is a React component."
    },
    key: null,
    ref: null,
    children: {
      type: "div",
      props: {
        className: "card",
        children: [
          {
            type: "h2",
            props: { children: "Hello" }
          },
          {
            type: "p",
            props: { children: "This is a React component." }
          }
        ]
      }
    }
  }
}

Finally, this component would look something like this in the DOM, which is similar to what the JSX in the Card component looked like:

<div class="card">
  <h2>Hello</h2>
  <p>This is a React component.</p>
</div>

How does React handle reactivity?

When interactions or programmatic changes update props or state, React re-runs the affected components, compares the new resulting virtual DOM with the previous one stored in memory, and then updates the parts of the real DOM that have changed. This process, known as reconciliation, allows React to avoid reloading the entire page while ensuring the UI stays in sync with state changes, but it comes at the cost of a bit of extra processing, as React needs to do this virtual DOM comparison before updating the real DOM.

JSX in Solid

While JSX was originally created by Meta (formerly Facebook) specifically for React, it has since been adopted by other libraries and frameworks. One such example is Solid, a JavaScript framework that also leverages JSX to simplify UI development. Because the core purpose of JSX remains the same in both React and Solid--making UI development more intuitive--the syntax is largely similar. In both cases, JSX follows the same general rules: components must have a single root element, tags must be properly closed, JavaScript expressions are wrapped in single curly braces.

How does Solid handle reactivity?

However, this is where the similarities end. Unlike React, Solid does not use a virtual DOM. Instead, Solid runs its components once to determine what the DOM should look like, without using a virtual DOM, then tracks reactive dependencies at a fine-grained level. When state changes, Solid updates only the specific parts of the DOM that are affected--without re-executing the entire component. This approach makes Solid highly efficient, as it avoids unnecessary re-computation and rendering overhead.

How does Solid manage this “fine-grained reactivity?” At its core, Solid uses signals, which consist of getter/setter pairs that allow state management, similar to useState in React. However, unlike React, where state changes trigger a component re-render, Solid tracks where a signal is accessed within JSX and updates only the necessary DOM elements when the signal changes.

Here’s our simple React example from above, but adjusted to work in Solid. It’s very similar to the React example and needed only a few small changes.

import { createSignal } from "solid-js";

function Card(props) {
  return (
    <div class="card">
      <h2>{props.title()}</h2>
      <p>{props.content()}</p>
    </div>
  );
}

function App() {
  const [title, setTitle] = createSignal("Hello");
  const [content, setContent] = createSignal("This is a Solid component.");

  return <Card title={title} content={content} />;
}

Transpiled and simplified, the JSX turns into something similar to the code seen below. The markup for Card is defined in _tmpl$ as a static template. Inside the Card function, Solid identifies which elements need to be reactive and creates references to those elements so it can efficiently update them if their associated signals change.

import { createSignal } from "solid-js";
import { template as _$template } from "solid-js/web";
import { insert as _$insert } from "solid-js/web";
import { createComponent as _$createComponent } from "solid-js/web";

// Static HTML template for the Card component
const _tmpl$ = _$template(`<div class="card"><h2></h2><p></p>`);

function Card(props) {
  return (() => {
    const _el$ = _tmpl$();               // Create the static template
    const _el$2 = _el$.firstChild;       // <h2>
    const _el$3 = _el$2.nextSibling;     // <p>

    // Insert reactive expressions into the DOM
    _$insert(_el$2, () => props.title());
    _$insert(_el$3, () => props.content());

    return _el$;
  })();
}

function App() {
  const [title, setTitle] = createSignal("Hello");
  const [content, setContent] = createSignal("This is a Solid component.");

  // Create and render the Card component
  return _$createComponent(Card, { title, content });
}

export default App;

Solid also tracks dependencies inside its createEffect and createMemo functions, ensuring that computations inside those functions automatically re-run when their dependencies change. Just as signals bear some resemblance to React’s useState, createEffect and createMemo are conceptually similar to React’s useEffect and useMemo, although Solid’s approach eliminates the need for dependency arrays and prevents unnecessary component executions. Instead of re-running an entire component function when state updates, Solid updates only the specific parts of the UI that depend on changed state.

We can see how Solid’s createEffect and React’s useEffect compare in another, slightly more complex example. Consider this React component:

import React, { useState, useEffect } from "react";

function ReactDogViewer() {
  const [breed, setBreed] = useState("corgi");
  const [dogImages, setDogImages] = useState([]);

  useEffect(() => {
    fetch(`https://dog.ceo/api/breed/${breed}/images/random/3`)
      .then((response) => response.json())
      .then((data) => {
        setDogImages(data.message);
      })
      .catch((error) => {
        console.error("Error fetching dogs:", error);
      });
  }, [breed]); // dependency array is essential here

  return (
    <div>
      <h2>Dog Breed Viewer</h2>

      <div>
        <select value={breed} onChange={(e) => setBreed(e.target.value)}>
          <option value="corgi">Corgi</option>
          <option value="labrador">Labrador</option>
          <option value="poodle">Poodle</option>
          <option value="beagle">Beagle</option>
        </select>
      </div>

      <div>
        {dogImages.map((url, index) => (
          <img
            key={index}
            src={url}
            alt={`${breed} ${index + 1}`}
            style={{ width: "200px", margin: "5px" }}
          />
        ))}
      </div>
    </div>
  );
}

export default ReactDogViewer;

In this example, we’re fetching 3 dog images from the Dog CEO API according to the breed selected from the dropdown menu, with “corgi” as the default. The useEffect hook ensures that the dog images are fetched initially when the component mounts, as well as whenever the breed state changes due to user selection. Specifying breed in the useEffect’s dependency array is essential to ensure that new dog data is fetched when the breed changes.

Here’s the SolidJS version of the same component:

import { createSignal, createEffect, For } from 'solid-js';

function SolidDogViewer() {
  const [breed, setBreed] = createSignal('corgi');
  const [dogImages, setDogImages] = createSignal([]);

  createEffect(() => {
    fetch(`https://dog.ceo/api/breed/${breed()}/images/random/3`)
      .then(response => response.json())
      .then(data => {
        setDogImages(data.message);
      })
      .catch(error => {
        console.error('Error fetching dogs:', error);
      });
  }); // no dependency array needed

  return (
    <div>
      <h2>Dog Breed Viewer</h2>
      
      <div>
        <select value={breed()} onChange={(e) => setBreed(e.target.value)}>
          <option value="corgi">Corgi</option>
          <option value="labrador">Labrador</option>
          <option value="poodle">Poodle</option>
          <option value="beagle">Beagle</option>
        </select>
      </div>
      
      <div>
        <For each={dogImages()}>
          {(url, index) => (
            <img 
              src={url} 
              alt={`${breed()} ${index() + 1}`} 
              style={{ width: '200px', margin: '5px' }}
            />
          )}
        </For>
      </div>
    </div>
  );
}

export default SolidDogViewer;

In the SolidJS component, createEffect replaces React’s useEffect, but it does not require any dependencies to be specified, because Solid already is tracking the breed() signal and knows to re-run createEffect any time breed changes. (Also, note that Solid’s <For> component replaces JavaScript’s .map() function to render the list of images.)

Key Takeaways

While both React and Solid use JSX to define components, their approaches to reactivity and rendering are fundamentally different. Solid’s JSX uses signals to maximize performance, automatically tracking where they’re used to update only the necessary DOM elements--eliminating the need for explicit dependency declarations. In contrast, React relies on component re-renders and the virtual DOM, which can introduce performance overhead. Solid’s direct DOM updates make it more efficient, especially for highly interactive applications. If you’re comfortable with JSX but want a more efficient way to manage UI updates, Solid offers an interesting alternative worth exploring.

Want to talk about how we can work together?

Katie can help

A portrait of Vice President of Business Development, Katie Jennings.

Katie Jennings

Vice President of Business Development