The race for innovation between frontend frameworks has been evolving for quite some time now. While React has innovated with features like JSX, which makes expressing UI logic more declarative, Svelte has introduced compilation to reduce the size of client bundles. On the other hand, SolidJS combines these ideas, along with the smart use of composable primitives and observables.
Parallel to the innovation of these frameworks is the evolution of the meta-frameworks built on top of them, like Next.js for React, SvelteKit for Svelte, and more recently, SolidStart for SolidJS. In this article, we’ll explore SolidStart, consider its features and use cases, and finally, build a simple app. To follow along, you can access the full code for the tutorial on GitHub. Let’s get started!
Trips
componentTrips
componentSolidJS offers many of the features you’d expect from a meta-framework, including file-based routing, API endpoints, and support for server-side, client-side, and static rendering.
SolidStart also comes equipped with some amazing, unique features, like the ability to use forms to trigger server actions, similar to RemixJS, as well as the ability to easily define RPC functions using its $server
function.
It’s important to note that at the time of writing, SolidStart is still in experimental status, and many features may be missing or incomplete. It’s advised to use SolidStart at your own risk. With that said, we can still use SolidStart to set up a simple app.
In this tutorial, we’ll build a simple application that stores data about business trips, including mileage and location. To begin, you should already have Node.js installed on your machine.
First, open up your code editor to an empty folder. If you don’t already have pnpm installed, run npm install -g pnpm
. Then, run pnpm create solid
.
When prompted to add server-side rendering, select yes. Select no for TypeScript, then choose a bare template. Once the app is generated, run pnpm install
.
When using SolidStart, the code you’ll work with will live in the /src
folder, which in itself contains some files and folders that you should be familiar with.
/components
: Store all components that aren’t pages in the /components
folder/routes
: Store components that are pages in /routes
. A page would be the default export of that fileroot/entry-client/entry-server
: Store files that handle the application startup, which we don’t need to touchCreate a /lib
folder, which you’ll use to create supporting files, for example, a lot of our API implementation details, support functions, and more.
Instead of using a database, we’ll use an array to demonstrate more simply how to work with our data models. If you’d rather use MongoDB, you can check out this article.
Create a file called src/lib/trips.js
:
// The Trips Array const trips = [ { location: "Miami, FL", mileage: 80, }, { location: "Savannah, GA", mileage: 120, }, ]; // functions for working with trips we can then convert into API routes or RPC calls export function getTrips(){ return trips } export function createTrip(newTrip){ trips.push(newTrip) return trips } export function updateTrip(id, updatedTrip){ trips[id] = updatedTrip return trips } export function deleteTrip(id){ trips.splice(id, 1) return trips }
In this file, we’ve created a trip
array to hold our data, as well as functions for working with our array:
getTrips
: Returns the array of trips, similar to SELECT * FROM table
in SQLcreateTrip
: Takes in an object and creates a new trip by pushing it into an array, simulating a INSERT INTO table VALUES (…)
query in a databaseupdateTrip
: Updates a trip in the array, similar to UPDATE table WHERE conditions SET updates
querydeleteTrip
: Deletes a trip in the array, similar to DELETE FROM table WHERE conditions
queryThese functions essentially will simulate having a data model from a database ORM.
Now, we can make these functions usable to our application in two ways. For one, we can write API routes that use these functions. Alternately, in relevant components, we can define RPC calls using these functions with the $server
function. Keep in mind that RPC, a Remote Procedure Call, refers to when a function that is run on the server is called from a function call on the client.
Any route can be an API endpoint. The route file just needs to export an async GET
/POST
/PUT
/DELETE
function to handle that type of request for that route. If the route renders a page by export defaulting a component, then you can’t define an additional GET
for that route.
To show an example of this, we’ll create a file called src/routes/api/trips/(trips).js
. Notice that the file name has parenthesis around it. This is a feature of SolidStart that allows you to denote the main file in a folder for a route. So, this file would handle the /api/trips
URL. Normally, we would have to name the file index.jsx
. After a while, having so many files with the same name gets confusing.
Place the following code in the index.jsx
file:
// json function for sending json responses import { json } from "solid-start"; import { getTrips, createTrip } from "~/lib/trips"; export async function GET(){ // return the array of trips return json(getTrips()) } export async function POST({request}){ // get the request body const body = await new Response(request.body).json() // create new trip createTrip(body) // return all trips return json(getTrips()) }
Now, start your server with npm run dev
, and you should be able to use something like Postman or Insomnia to test the routes.
Make a GET
request to http://localhost:3000/api/trips/
, and you should get all your trips back:
Make a POST
request to http://localhost:3000/api/trips/
with a JSON body like the one below, and you should see the trip added:
{ "location": "Manchester,ct", "mileage": 1000 }
Awesome, we now have working API endpoints, how easy was that!
In SolidStart, we can pre-fetch data for the route that is available to the page
route and all the child components.
We export routeData
, and its return value becomes available to the page and subcomponents via the useRouteData
Hook.
Let’s set up our main page src/routes/index.jsx
to create route data as the result of an API call to our API Route:
import { createRouteData } from "solid-start"; // define our route data, server provided data to frontend export function routeData() { return createRouteData(async () => { // fetch data from api endpoint const response = await fetch("http://localhost:3000/api/trips") const data = await response.json() return data }); } export default function Home() { return ( <main> </main> ); }
Notice that we used the createRouteData
function. This function works a lot like React Query, where we can wrap an asynchronous action with the following benefits:
We’ll see in our trip
component how we can use actions and route data.
Trips
componentNow, we’ll create a file to put this all together in src/components/Trips.jsx
:
import { createRouteAction, useRouteData} from "solid-start"; import { createTrip } from "~/lib/trips"; import server$ from "solid-start/server"; export default function Trips() { // bring the route data into our component const trips = useRouteData(); // Define an RPC call of what we want to run on the server const makeTrip = server$(async (trip) => createTrip(trip)) // define a form for creating a trip using solid-states action system const [_, { Form }] = createRouteAction(async (formData) => { // create the new trip object const trip = { location: formData.get("location"), mileage: formData.get("mileage") } // pass object RPC call to create new trip on server makeTrip(trip) }); return ( <div> <ul> {trips()?.map((trip) => ( <li>{trip.location} - mileage: {trip.mileage}</li> ))} </ul> <Form> <input type="input" name="location" placeholder="location"/> <input type="number" name="mileage" placeholder="mileage"/> <input type="submit"/> </Form> </div> ); }
In this component, we use many SolidStart features; for one, we use useRouteData
to get the routeData
defined in the page
component. server$
defines a function that runs solely on the server. In this case, we want the function that creates Trips
to run only on the server since it wouldn’t work on the client.
Finally, createRouteAction
creates a function and the corresponding Form
component. The Form
component calls the function that acts as an action, triggering a refetch of routeData
.
Trips
componentEdit your src/routes/index.jsx
as follows:
import { createRouteData } from "solid-start"; import Trips from "~/components/Trips"; // define our route data, server provided data to frontend export function routeData() { return createRouteData(async () => { // fetch data from api endpoint const response = await fetch("http://localhost:3000/api/trips") const data = await response.json() return data }); } export default function Home() { return ( <main> <Trips/> </main> ); }
Using the useRouteData
Hook, we export the routeData()
function, which allows us to pre-fetch data to be used by the page and it’s subcomponents.
The createRouteData
creates a resource that will refetch anytime an action like the one our form triggers on submit
occurs. Finally, the <Trips/>
component displays our trips and form.
Hopefully, you’ve gotten a sense of just how powerful the SolidStart framework can be. Although it is still experimental at the time of writing, SolidStart has a promising future. If you want to see other variations of what you can do with SolidStart, check out the following builds:
There’s no doubt that frontends are getting more complex. As you add new JavaScript libraries and other dependencies to your app, you’ll need more visibility to ensure your users don’t run into unknown issues.
LogRocket is a frontend application monitoring solution that lets you replay JavaScript errors as if they happened in your own browser so you can react to bugs more effectively.
LogRocket works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.
Build confidently — start monitoring for free.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.