Solid Start 1.0 is Finally Here!
What’s interesting though, is that despite the apparent variety in the space, when you really think about it, the majority of frameworks offer the same signal based reactivity paired with a templating solution and powerful server rendering support.
Solid Start does a great job in cherry picking the best ideas the ecosystem has to offer, so let’s spend the next couple of minutes reviewing 7 Solid Start concepts while building a demo app.
First, let’s make sure you are running on an up to date Node version, and then go through Solid’s project setup wizard.
$ nvm use v18
$ npm create solid
With the project in place, we’ll work in the following files. App.tsx is the main entry point into our application, we’ll store Solid components under components directory, and we’ll use the routes folder to define our file system based router. The router structure is pretty intuitive, with user routes being mapped to the files defined on the disk.
Next, let’s jump into the app file, and write some code.
By the way, you are following ”The Snippet”, the fast-paced, no BS series where we are analyzing various code snippets to better understand useful software concepts.
Solid Reactivity & Signals
The App component is just a wrapper around the Solid router which traverses the routes directory, and makes them accessible using the FileRoutes component.
// app.tsx
export default function App() {
return (
<Router root={(props) => <Suspense>{props.children}</Suspense>}>
<MetaProvider>
<FileRoutes />
</MetaProvider>
</Router>
);
}
Next, let’s jump into index.tsx, which will be rendered when users access the root path.
// routes/index.tsx
export default function Home() {
// reactive state
// jsx template
}
In Solid, components are plain functions which group together reactive state with JSX templating.
To create a TODO app, we’ll start by defining a list of tasks, and a title field which will be used to add new tasks to the list.
// routes/index.tsx
export default function Home() {
const [tasks, setTasks] = createSignal<Task[]>([]);
const [title, setTitle] = createSignal("");
// jsx template
}
Of course, we are using Signals here, since this became the status quo for modern reactivity.
Then, in the JSX part we’ll iterate over the tasks using Solid’s For component, and then render a TaskLine component which accepts a task entity as a property.
// routes/index.tsx
export default function Home() {
const [tasks, setTasks] = createSignal<Task[]>([]);s
const [title, setTitle] = createSignal("");
return (
<main>
<For each={tasks()}>{task => (
<TaskLine task={task} />
)}</For>
<footer>
<input value={title()} onChange={ev => setTitle(ev.target.value)} />
<button onClick={save}>Add</button>
</footer>
</main>
);
}
Note that Solid removes the overhead of a virtual DOM, and performs UI updates directly in the real DOM. However, in order to ensure an optimized behavior, special components like For or Show have to be used.
Next, in the footer, we’ll capture the user input using the onChange listener, and listen for button click events to call the save function. Here, we’ll define a unique id for each task entry, and then push that entry into the lists of tasks.
// routes/index.tsx
export default function Home() {
const [tasks, setTasks] = createSignal<Task[]>([]);s
const [title, setTitle] = createSignal("");
function save() {
const task = { id: createUniqueId(), title: title() } as Task;
setTasks([...tasks(), task]);
setTitle("");
}
return (
<main>
<For each={tasks()}>{task => (
<TaskLine task={task} />
)}</For>
<footer>
<input value={title()} onChange={ev => setTitle(ev.target.value)} />
<button onClick={save}>Add</button>
</footer>
</main>
);
}
This file contains a Task type and a TaskLine component which are yet to be defined, so let’s jump into the Task component file to continue our implementation.
Task is a straightforward type with an id, a title, some details and a done status.
// components/Task.tsx
export type Task = {
id: string;
title: string;
details: string;
done: boolean;
}
Since the TaskLine component accepts some properties, we’ll enforce type safety here through Task Line Props.
// components/Task.tsx
export type TaskLineProps = {
task: Task;
};
export function TaskLine (props: TaskLineProps) {
return <div>
<header>
<input type="checkbox" checked={props.task.done} />
<a href={`/task/${props.task.id}`}>{props.task.title}</a>
</header>
<p>{props.task.details}</p>
</div>
}
The component itself is presentational, and we are simply rendering some information on the screen. There are two things worth mentioning here. First, we can link to a task details page, which will then be mapped in the routes folder. Second, whenever a task is completed, we need to notify the parent component, and this component communication is usually done in Solid by passing a handler from the parent to the child.
Before moving to the API, let’s quickly jump into the task details file to look at the interaction with the server.
Server Functions
First, note that the task id is passed as a path variable, so we can retrieve it via the useParam.
// routes/task/[id].tsx
const getTask = cache (async (id: string) => {
"use server";
return store.getTask(id) as Task;
}, "tasks");
export const route = {
load: (id: string) => getTask(id),
};
export default function TaskDetails() {
const params = useParams();
const task = createAsync(() => getTask(params.id));
return <article>
<h3>{task()?.title}</h3>
<p>{task()?.details}</p>
</article>
}
Then, in a real world scenario, your tasks will be stored somewhere on the server as well. An advantage of being a full-stack JavaScript framework is that it is easy to write data loading code that can run both on the server and client.
So, through “use server” we tell the bundler to create an RPC instance, and not include this code in the client bundle.
Now that we’ve seen how data can be retrieved from the server, let’s now look at how we can send and store data using REST calls. Under the API folder let’s create a new tasks file which exposes a POST function. Here we’ll retrieve the request body, perform validations if necessary, and persist the data.
// routes/api/tasks.ts
export async function post({request}: APIEvent) {
const body = await request.json(s);
store.save(body.id, body.title);
return new Response();
}
Then, in the index.ts file we can simply perform a fetch call when the save button is clicked, and pass in the task details in the request body.
// routes/index.tsx
function saveTask(task: Task) {
fetch("/api/tasks", {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(task)
})
}
export default function Home() {
const [tasks, setTasks] = createSignal<Task[]>([]);s
const [title, setTitle] = createSignal("");
function save() {
const task = { id: createUniqueId(), title: title() } as Task;
// fetch call
saveTask(task)
setTasks([...tasks(), task]);
setTitle("");
}
return (
<main>
<For each={tasks()}>{task => (
<TaskLine task={task} />
)}</For>
<footer>
<input value={title()} onChange={ev => setTitle(ev.target.value)} />
<button onClick={save}>Add</button>
</footer>
</main>
);
}
Of course, we discussed just the basics in this article, so please let me know in the comments if you are interested in a deep dive on this topic.
If you feel like you learned something, you should watch some of my youtube videos or subscribe to the newsletter.
Until next time, thank you for reading!