A First Look at SolidStart

ajcwebdev - Nov 23 '22 - - Dev Community

Outline

All of this project's code can be found in the First Look monorepo on my GitHub.

Introduction

On November 9, 2022, Ryan Carniato published Introducing SolidStart: The SolidJS Framework to announce the beta launch of SolidStart, the eagerly awaited metaframework for SolidJS. SolidStart provides a first-party project starter and metaframework for SolidJS. It prescribes a recommended way to organize, architect, and deploy SolidJS applications.

Vite is included for the build tool along with extra Solid packages for common functionality and a CLI for generating templated projects. These packages enable features such as routing, MetaTags, different rendering modes, TypeScript support, and deployment adapters. In addition to Node and static hosting, adapters currently exist for the following platforms:

A History of SolidJS and How it Compares to React

Before diving into SolidStart, it's worth taking a moment to outline the history and motivation behind the creation of Solid. Branded as "a reactive JavaScript library for building user interfaces," Ryan open sourced the framework on April 24, 2018. It was designed as a spiritual successor to the reactive programming model exemplified by KnockoutJS.

React wasn’t the first JavaScript “Just a Render Library”. I attribute that honor to a much older library, KnockoutJS. Knockout didn’t have components but identified an application was built from 3 parts, Model, ViewModel, and View, and only cared about how you organized the latter. The ViewModel was a revolutionary concept.

ViewModels are instances much like components. But what set Knockout apart was ViewModels could be anything you wanted; an object, a function, a class. There were no lifecycle functions. You could bring your own models and organize your application as you saw fit. Without best practices it could be a complete mess.

But it was truly “Just a Render Library.” Those boundaries haven’t changed in over a decade... As Controllers transformed to Routers, Models to Stores, and ViewModels/Views got wrapped together as Components, the anatomy of a Component (even in a smaller library like React) is still 3 main parts:

  • Container
  • Change (Local State) Manager
  • Renderer

Ryan Carniato - B.Y.O.F. Writing a JS Framework in 2018 (November 10, 2018)

As the 2010s progressed, Ryan believed the JavaScript world had moved on from using composable reactive primitives in favor of class components and lifecycle methods. Dissatisfied with this direction, Ryan aimed for Solid to be a more modern reactive framework inspired by Knockout but with features informed by newer component frameworks like Angular, React, and Vue.

Shortly after the framework's release, React introduced hooks. Their debut in React Today and Tomorrow and 90% Cleaner React With Hooks, October 26, 2018 became a pivotal moment for Solid. React hooks are functions that can access React state and lifecycle methods from functional components. These functions can be composed into more complex UIs much like Solid.

Within a few years, the majority of React developers would be developing with composable, functional patterns. This ensured that React developers would find Solid easily comprehensible. But despite the surface level similarities between Solid and React's syntax, Solid has a few key advantages over React due to its underlying implementation.

Solid removes the need for some of React's more complex hooks that patch over the leaky abstraction underlying React. useCallback exists to give React developers a mechanism for preventing rerenders. But in Solid, components only mount once and don't rerender so there is no need for an equivalent hook.

SolidJS Benchmark Performance

Solid is also one of the most performant JavaScript libraries. This is evidenced by the results of the JS Framework Benchmark. A large, randomized table of entries is created and modified. Rendering duration is measured along with how long various operations take to complete (lower scores are better).

While admittedly a contrived example, the benchmark allows factoring multiple measurements into a single geometric mean representing comparative performance between frameworks. For a combination of all types of read and write operations, Solid ranks just below vanilla JavaScript:

Framework Version Mean
Vanilla JS N/A 1.00
Solid 1.5.4 1.10
Lit-html 1.1.0 1.19
Vue 3.2.37 1.25
Svelte 3.50.1 1.30
Preact 10.7.3 1.43
Angular 13.0.0 1.58
Marko 4.12.3 1.70
React 18.2.0 1.73

Solid ties vanilla JS and ranks just below Svelte on lighthouse startup metrics:

Framework Version Mean
Svelte 3.50.1 1.03
Solid 1.5.4 1.04
Vanilla JS N/A 1.04
Preact 10.7.3 1.06
Lit-html 1.1.0 1.07
Marko 4.12.3 1.18
Vue 3.2.37 1.27
React 18.2.0 1.69
Angular 13.0.0 1.77

SolidStart Motivations

SolidStart takes influence from other JavaScript metaframeworks including Next.js, Nuxt.js, and SvelteKit by introducing multiple build modes, routing conventions, opinionated project structures, and pre-configured deployment adapters. The framework can produce sites or applications that employ either:

In the future, you'll also be able to choose:

Create Client Rendered Solid Project

mkdir ajcwebdev-solidstart
cd ajcwebdev-solidstart
pnpm init
pnpm add solid-js @solidjs/meta @solidjs/router solid-start
pnpm add -D solid-start-node vite-plugin-solid vite undici typescript
Enter fullscreen mode Exit fullscreen mode

Add vite scripts to package.json and set type to module.

{
  "name": "ajcwebdev-solidstart",
  "version": "1.0.0",
  "description": "An example SolidStart application deployed on Netlify, Vercel, and Cloudflare Pages",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "serve": "vite preview"
  },
  "keywords": [ "SolidJS", "SolidStart", "Netlify", "Vercel", "Cloudflare" ],
  "author": "Anthony Campolo",
  "license": "MIT"
}
Enter fullscreen mode Exit fullscreen mode

Create a .gitignore file.

echo 'node_modules\n.env\n.DS_Store\ndist\n.solid\nnetlify\n.netlify\n.vercel' > .gitignore
Enter fullscreen mode Exit fullscreen mode

There's only a handful of files needed for a working Solid project. These include a configuration file for Vite (vite.config.ts), an entry point for our JavaScript application (src/root.tsx), and an entry point for our HTML page (index.html).

Run the following commands:

mkdir src
echo > tsconfig.json   # TypeScript Configuration
echo > vite.config.ts  # Vite Configuration
echo > index.html      # HTML entry point where JavaScript app loads
echo > src/root.css    # CSS stylesheet
echo > src/root.tsx    # Defines the document the app renders
Enter fullscreen mode Exit fullscreen mode

In addition to the required files we also created optional files for CSS styling and TypeScript configuration. Later in the tutorial when we migrate this project to SolidStart, we'll remove the HTML file and replace it with two files, entry-server.tsx and entry-client.tsx.

TypeScript and Vite Project Configuration

Copy/paste this impenetrable hunk of gibberish into your tsconfig.json and say a prayer to Microsoft.

{
  "compilerOptions": {
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "strict": true,
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "node",
    "jsxImportSource": "solid-js",
    "jsx": "preserve",
    "types": ["vite/client"],
    "baseUrl": "./",
    "paths": {
      "~/*": ["./src/*"]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The vite.config.ts file is where we'll add the Solid Plugin to define our Vite Configuration. Import solidPlugin from vite-plugin-solid and add it to the plugins array inside Vite's defineConfig helper.

// vite.config.ts

import solidPlugin from "vite-plugin-solid"
import { defineConfig } from "vite"

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

HTML Entry, CSS Styling, and Render Function

The root Solid component will be imported as an ESM module from /src/root.tsx and set to the src attribute.

<!-- index.html -->

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="description" content="An example SolidJS single-page application." />
    <title>A First Look at SolidStart</title>
  </head>

  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <script src="/src/root.tsx" type="module"></script>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Include the following CSS styles in src/root.css.

/* src/root.css */

body {
  background-color: #282c34;
  color: white;
  font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
  margin: 0;
  text-align: center;
}

header {
  font-size: calc(10px + 2vmin);
  margin: 1rem;
}

a {
  color: #b318f0;
}
Enter fullscreen mode Exit fullscreen mode

To mount Solid and automatically create a reactive root, import render from solid-js/web and pass in two arguments, a top-level component function and an element to mount on:

  • The first argument returns the root component and must be passed a function.
  • The mounting container is passed for the second argument and wired up to the root div.
// src/root.tsx

/* @refresh reload */
import { render } from "solid-js/web"
import "./root.css"

function App() {
  return (
    <>
      <header>
        <h1>A First Look at SolidStart</h1>
        <a href="https://github.com/solidjs/solid">Learn Solid</a>
      </header>
      <footer>
        <span>
          Visit <a href="https://ajcwebdev.com">ajcwebdev.com</a> for more tutorials
        </span>
      </footer>
    </>
  )
}

render(
  () => <App />, document.getElementById('root') as HTMLElement
)
Enter fullscreen mode Exit fullscreen mode

Start Development Server

pnpm dev # or npm run dev | yarn dev
Enter fullscreen mode Exit fullscreen mode

Open localhost:5173 to view the running application in your browser. The page will reload if you make edits.

01 - Solid homepage with styling on localhost 5173

At this point, we could build and deploy a dist folder to any static host provider. Run pnpm build and pnpm serve to test serving your bundle on localhost:4173. Instead of deploying this project, we'll continue to the next section and begin modifying our project to make it work with SolidStart.

Migrate Project to SolidStart

First, delete your existing HTML file and create a routes directory inside src.

rm -rf index.html
mkdir src/routes
echo > src/entry-server.tsx # Server entry point
echo > src/entry-client.tsx # Browser entry point
echo > src/routes/index.tsx # Home route
Enter fullscreen mode Exit fullscreen mode

SolidStart Scripts and Vite Configuration

Replace the Vite scripts with the following SolidStart scripts.

{
  "scripts": {
    "dev": "solid-start dev",
    "build": "solid-start build",
    "start": "solid-start start"
  },
}
Enter fullscreen mode Exit fullscreen mode

Remove solidPlugin from vite-plugin-solid and replace it with solid from solid-start/vite.

// vite.config.ts

import solid from "solid-start/vite"
import { defineConfig } from "vite"

export default defineConfig({
  plugins: [solid()]
})
Enter fullscreen mode Exit fullscreen mode

Index Route, Root, and Entry Points

Copy the App component from src/root.tsx and include it in src/routes/index.tsx with export default.

// src/routes/index.tsx

export default function App() {
  return (
    <>
      <header>
        <h1>A First Look at SolidStart</h1>
        <a href="https://github.com/solidjs/solid">Learn Solid</a>
      </header>
      <footer>
        <span>
          Visit <a href="https://ajcwebdev.com">ajcwebdev.com</a> for more tutorials
        </span>
      </footer>
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

root.tsx is the point where your code runs on both the server and client. It exports a Root component that is shared on the server and browser as an isomorphic entry point to your application. Since SolidStart is designed for file-system routing, routes are defined via a folder structure under the /routes folder. You can pass them into the <Routes> component with the <FileRoutes> component.

This collects routes from the file-system in the /routes folder to be inserted into a parent <Routes> component. Since <FileRoutes> returns a route configuration, it must be placed directly between <Routes>, typically in the root.tsx file. <Routes> is a special Switch component that renders the correct <Route> child based on the users' location, and switches between them as the user navigates.

// src/root.tsx

// @refresh reload
import { Suspense } from "solid-js"
import { Body, ErrorBoundary, FileRoutes, Head, Html, Meta, Routes, Scripts, Title } from "solid-start"
import "./root.css"

export default function Root() {
  return (
    <Html lang="en">
      <Head>
        <Title>A First Look at SolidStart</Title>
        <Meta charset="utf-8" />
        <Meta name="viewport" content="width=device-width, initial-scale=1" />
        <Meta name="description" content="An example SolidStart application deployed on Netlify, Vercel, and Cloudflare Pages." />
      </Head>
      <Body>
        <ErrorBoundary>
          <Suspense fallback={<div>Loading...</div>}>
            <Routes>
              <FileRoutes />
            </Routes>
          </Suspense>
        </ErrorBoundary>
        <Scripts />
      </Body>
    </Html>
  )
}
Enter fullscreen mode Exit fullscreen mode

entry-client.tsx starts the application in the browser by passing <StartClient> to a mount function. mount is an alias over Solid's hydrate and render methods. It ensures that the client always starts up properly whether you are using SolidStart for client-only rendering or server-side rendering.

// src/entry-client.tsx

import { mount, StartClient } from "solid-start/entry-client"

mount(() => <StartClient />, document)
Enter fullscreen mode Exit fullscreen mode

entry-server.tsx starts the application on the server by passing <StartServer> to a render function called renderAsync. createHandler enables a mechanism for introducing middleware into server rendering. <StartServer> wraps the application root and includes Context providers for Routing and MetaData. It takes the Event object originating from the underlying runtime and includes information such as the request, responseHeaders and status codes.

// src/entry-server.tsx

import { StartServer, createHandler, renderAsync } from "solid-start/entry-server"

export default createHandler(
  renderAsync((event) => <StartServer event={event} />)
)
Enter fullscreen mode Exit fullscreen mode

While this example uses renderAsync, there are three different render functions provided by SolidStart. Each wraps Solid's rendering method and returns a unique output:

  • renderSync calls renderToString to synchronously respond immediately and render the application to a string.
  • renderAsync calls renderToStringAsync to asynchronously respond when the page has fully been loaded and render the application to a promise.
  • renderStream calls renderToStream to asynchronously respond as soon as it can and render the application to a ReadableStream.

Check that everything still displays as expected.

pnpm dev
Enter fullscreen mode Exit fullscreen mode

Open localhost:3000.

Components and Reactive Primitives

Since this is a tutorial about a frontend framework it will be considered illegitimate until we build a counter. Create a components directory and then a file called Counter.tsx inside of it.

mkdir src/components
echo > src/components/Counter.tsx
Enter fullscreen mode Exit fullscreen mode

There are two foundational building blocks at the core of Solid's fine grained reactivity that will enable us to craft this magnificent counter into existence:

  • Components contain stateless DOM elements within functions that accept props and return JSX elements.
  • Reactive Primitives including Signals, Effects, and Memos track and broadcast the changing values that represent the state of the components over time.

In this component we'll create a signal to track the changing value of the counter and an effect to modify the value with button clicks.

Create Signal

Signals contain values that change over time. They are tracked by the framework and update automatically by broadcasting to the rest of the interface. Use createSignal to initialize a value of 0 and set it to count. Increment the counter once every second by passing a setCount(count() + 1) function to a setInterval() method that executes every 1000 milliseconds.

// src/components/Counter.tsx

import { createSignal } from "solid-js"

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

  setInterval(() => setCount(count() + 1), 1000)

  return (
    <>The count is now: {count()}</>
  )
}
Enter fullscreen mode Exit fullscreen mode

Inside src/routes/index.tsx, import Counter from ../components/Counter. Return a <Counter /> component in the return function of the App component.

// src/routes/index.tsx

import Counter from "../components/Counter"

export default function App() {
  return (
    <>
      <header>
        <h1>A First Look at SolidStart</h1>
        <a href="https://github.com/solidjs/solid">Learn Solid</a>
      </header>
      <main>
        <Counter />
      </main>
      <footer>
        <span>
          Visit <a href="https://ajcwebdev.com">ajcwebdev.com</a> for more tutorials
        </span>
      </footer>
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

02 - Homepage with counter

Create Effect

An Effect is an example of an observer that runs a side effect depending on a signal. createEffect creates a new computation (for example to modify the DOM manually) and runs the given function in a tracking scope.

// src/components/Counter.tsx

import { createSignal, createEffect } from "solid-js"

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

  createEffect(() => count())

  return (
    <>
      <button onClick={() => setCount(count() + 1)}>
        Click Me
      </button>
      <div>The count is now: {count()}</div>
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

This automatically tracks the dependencies and reruns the function whenever the dependencies update.

03 - Create effect button

Create Route Data

Create a new file called students.tsx inside the src/routes directory. This will contain a third party API call to return a list of Harry Potter characters.

echo > src/routes/students.tsx
Enter fullscreen mode Exit fullscreen mode

createRouteData is a wrapper over createResource for handling async data fetching and refetching. With SolidStart's file system routing, components defined under /routes can utilize a routeData function which executes when navigation to that component begins. This hook returns the JSON parsed data from the loader function. We'll use the Fetch API to query Harry Potter information on Deno Deploy.

// src/routes/students.tsx

import { useRouteData, createRouteData } from "solid-start"

type Student = { name: string; }

export function routeData() {
  return createRouteData(async () => {
    const response = await fetch("https://hogwarts.deno.dev/students")
    return (await response.json()) as Student[]
  })
}

export default function Page() { }
Enter fullscreen mode Exit fullscreen mode

This routeData function can be thought of like a "loader" function (what a brilliant idea, amazing no one thought of it before) which includes a useRouteData hook to access the returned data.

// src/routes/students.tsx

import { useRouteData, createRouteData } from "solid-start"

type Student = { name: string; }

export function routeData() {
  return createRouteData(async () => {
    const response = await fetch("https://hogwarts.deno.dev/students")
    return (await response.json()) as Student[]
  })
}

export default function Page() {
  const students = useRouteData<typeof routeData>()

  return (
    <>
      <header>
        <h1>Students</h1>
      </header>
      <main>
        <code>{JSON.stringify(students(), null, 2)}</code>
      </main>
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

useRouteData can use whatever data is returned from the routeData loader function.

04 - Return student data to page as raw JSON

The <For> component loops over an array of objects. The <For> component has only one prop, each, which is passed an array to loop over with a callback similar to JavaScript's map callback.

// src/routes/students.tsx

import { useRouteData, createRouteData } from "solid-start"
import { For } from "solid-js"

type Student = { name: string; }

export function routeData() {
  return createRouteData(async () => {
    const response = await fetch("https://hogwarts.deno.dev/students")
    return (await response.json()) as Student[]
  })
}

export default function Page() {
  const students = useRouteData<typeof routeData>()

  return (
    <>
      <header>
        <h1>Students</h1>
      </header>
      <main>
        <For each={students()}>
          {student => <li>{student.name}</li>}
        </For>
      </main>
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

05 - Loop over json response and display data with for component

API Routes

API routes are similar to other routes except instead of exporting a default Solid component with a routeData function, they export functions that are named after the HTTP methods they handle such as GET or POST.

mkdir src/routes/api
echo > src/routes/api/index.ts
Enter fullscreen mode Exit fullscreen mode

json is a helper function to send JSON HTTP responses. Return a JSON object with the key set to hello and the value set to world.

// src/routes/api/index.ts

import { json } from "solid-start"

export function GET() {
  return json(
    { hello: "world" }
  )
}
Enter fullscreen mode Exit fullscreen mode

Open 127.0.0.1:3000/api or make a request with cURL.

curl http://127.0.0.1:3000/api
Enter fullscreen mode Exit fullscreen mode
{"hello":"world"}
Enter fullscreen mode Exit fullscreen mode

Deployment Adapters

Here is the directory and file structure for the final project before including any files related to deployment:

.
├── src
│   ├── components
│   │   └── Counter.tsx
│   ├── routes
│   │   ├── api
│   │   │   └── index.ts
│   │   ├── index.tsx
│   │   └── students.tsx
│   ├── entry-client.tsx
│   ├── entry-server.tsx
│   ├── root.css
│   └── root.tsx
├── package.json
├── tsconfig.json
└── vite.config.ts
Enter fullscreen mode Exit fullscreen mode

Push your project to a GitHub repository.

git init
git add .
git commit -m "mr solid"
gh repo create ajcwebdev-solidstart \
  --description="An example SolidJS application deployed on Netlify, Vercel, and Cloudflare Pages." \
  --remote=upstream \
  --source=. \
  --public \
  --push
Enter fullscreen mode Exit fullscreen mode

If you only want to use a single deployment platform, select one of the next three options and push the changes to the main branch. I will deploy the project to all three by creating a different branch for each and specifying the correct branch on the deployment platform.

Deploy to Netlify

Import the netlify adapter from solid-start-netlify.

// vite.config.ts

// @ts-ignore
import netlify from "solid-start-netlify"
import solid from "solid-start/vite"
import { defineConfig } from "vite"

export default defineConfig({
  plugins: [solid({
    adapter: netlify({ edge: true })
  })]
})
Enter fullscreen mode Exit fullscreen mode

Install solid-start-netlify and the Netlify CLI.

pnpm add -D solid-start-netlify netlify-cli @types/node
Enter fullscreen mode Exit fullscreen mode

Create a netlify.toml file for the build instructions.

echo > netlify.toml
Enter fullscreen mode Exit fullscreen mode

Set the command to pnpm build and the publish directory to netlify.

# netlify.toml

[build]
  command = "pnpm build"
  publish = "netlify"
Enter fullscreen mode Exit fullscreen mode

Connect the repository to your Netlify account through the Netlify dashboard or use the following commands with the Netlify CLI.

pnpm ntl login
pnpm ntl init
Enter fullscreen mode Exit fullscreen mode

The build commands will be automatically entered from the netlify.toml file.

pnpm ntl deploy --prod --build
Enter fullscreen mode Exit fullscreen mode

Open ajcwebdev-solidstart.netlify.app to see a running example.

Deploy to Vercel

Install solid-start-vercel and the Vercel CLI.

pnpm add -D solid-start-vercel vercel
Enter fullscreen mode Exit fullscreen mode

Import the vercel adapter from solid-start-vercel.

// vite.config.ts

// @ts-ignore
import vercel from "solid-start-vercel"
import solid from "solid-start/vite"
import { defineConfig } from "vite"

export default defineConfig({
  plugins: [solid({
    adapter: vercel({ edge: true })
  })]
})
Enter fullscreen mode Exit fullscreen mode

Deploy the project with the Vercel CLI.

pnpm vercel --yes --prod
Enter fullscreen mode Exit fullscreen mode

Open ajcwebdev-solidstart.vercel.app.

Deploy to Cloudflare

SolidStart includes two adapters for Cloudflare, one for Cloudflare Workers and another for Cloudflare Pages. It's important to note that the Cloudflare Pages adapter is also using Workers through Pages Functions. Install solid-start-cloudflare-pages and the wrangler CLI.

pnpm add -D solid-start-cloudflare-pages wrangler
Enter fullscreen mode Exit fullscreen mode

Import the cloudflare adapter from solid-start-cloudflare-pages.

// vite.config.ts

// @ts-ignore
import cloudflare from "solid-start-cloudflare-pages"
import solid from "solid-start/vite"
import { defineConfig } from "vite"

export default defineConfig({
  plugins: [solid({
    adapter: cloudflare({})
  })],
})
Enter fullscreen mode Exit fullscreen mode

Build the project's assets and run a local Worker emulation on localhost:8788 with wrangler pages dev.

pnpm wrangler login
pnpm build
pnpm wrangler pages dev ./dist/public
Enter fullscreen mode Exit fullscreen mode

Create a project with wrangler pages project create and deploy the project with wrangler pages publish.

pnpm wrangler pages project create ajcwebdev-solidstart \
  --production-branch production
pnpm wrangler pages publish dist/public \
  --project-name=ajcwebdev-solidstart \
  --branch=production
Enter fullscreen mode Exit fullscreen mode

Open ajcwebdev-solidstart.pages.dev.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .