SolidJS is fast becoming the center of attention in the web development community. With its straightforward state management, fine-grained reactivity, and high performance, Solid has put itself on a pedestal for other JavaScript frameworks.
Solid is everything React developers have been asking for, and in this article, I’ll walk you through building a task tracker with Solid.
To follow along with this tutorial, you’ll need knowledge of JavaScript and TypeScript, Node.js modules, and components in frontend frameworks.
If you’ve worked with React before, Solid will look very familiar. When React Hooks was first announced, I was so happy because I thought it would solve our state management crisis. Hooks made local state management in components easier, but global state management remained complex.
It was still difficult for disconnected components to share data and numerous libraries showed up to try and solve the state management problem — which increased development fatigue and added unnecessary complexity to our codebase.
I’ve also seen the same problem happen in other frontend frameworks; it’s as if global state management is an afterthought, rather than something that was planned for from the beginning.
With Solid, things are different. Global state management is as easy as creating state and exporting it. There’s no need for any complex setup or third-party library.
Solid also uses JSX, the popular HTML-like syntax extension to JavaScript. This makes handling UI logic, events, and state changes straightforward. Coupled with that, Solid compiles to plain JavaScript, so there’s no need for a virtual DOM, making it relatively faster than frameworks like React and Angular.
Solid also has a simple workflow. Components only render once, just like in JavaScript, so it’s easier to predict the outcome of your code.
Another huge advantage of Solid is that it builds on the shoulders of other web frameworks, so it proudly emulates the good parts and improves the not-so-good parts.
Let’s go ahead and setup our Solid app to learn how to build a web app with Solid step-by-step.
To set up a Solid app on your local machine, you’ll need to install Node.js. If you already have it installed, running the following command on your terminal should return your current Node.js version:
node --version
Next, let’s create a new Solid app by running the following command on our terminal:
npx degit solidjs/templates/ts task-tracker
Using solidjs/templates/ts
generates a Solid/TypeScript app. For JavaScript, you’ll have to change the command to solidjs/templates/js
.
After running the command, you should see a success message that looks like this:
> cloned solidjs/templates#HEAD to task-tracker
Now, go ahead and open the generated app in your code editor or IDE of choice. Here’s what the app structure should look like:
Notice that our Solid app uses Vite as its default build tool and pnpm as the default package manager. These tools combined provide a great development experience for component rendering, app startup time, and package management.
Our app component currently lives inside of the ./src/App.tsx
file:
import type { Component } from 'solid-js' ... const App: Component = () => { return ( <div> ... </div> ); } export default App
First, we import the Component
type from solid-js
which is then used as the type for our App
component.
Components in Solid are JavaScript functions. They are reusable and can be customized using props, which are similar to function parameters/arguments.
Inside of the ./src/index.tsx
file, we render our App
component:
import { render } from 'solid-js/web' import App from './App' render(() => <App />, document.getElementById('root') as HTMLElement)
The render()
method from solid-js/web
expects two arguments:
<App />
componentWhen you navigate to the ./index.html
file, you’ll see the root div
and the use of our ./src/index.tsx
file via the <script />
tag:
... <body> <noscript>You need to enable JavaScript to run this app.</noscript> <div id="root"></div> <script src="/src/index.tsx" type="module"></script> </body>
To run our Solid app, we’ll have to first install our packages by running the command pnpm install
on our terminal, and then pnpm dev
to start our application in development mode. You should see a success message that looks like this:
vite v2.9.9 dev server running at: > Local: http://localhost:3001/ > Network: use `--host` to expose ready in 378ms.
When you navigate to http://localhost:3001
or the displayed URL on your terminal, you should see a page similar to this:
Now that we’ve successfully set up our Solid app, let’s install Bootstrap for styling so that we don’t have to bother about CSS.
To install Bootstrap, run the following command on your terminal:
pnpm install bootstrap
Next, we’ll use the following line of code to import Bootstrap in our ./src/index.tsx
file:
import 'bootstrap/dist/css/bootstrap.min.css'
We can also remove the current ./index.css
import as we won’t be needing it. Our index.tsx
file should now look like this:
import { render } from 'solid-js/web' import App from './App' import 'bootstrap/dist/css/bootstrap.min.css' render(() => <App />, document.getElementById('root') as HTMLElement)
Let’s use JSX to structure our task tracker. Inside the ./src/App.tsx
file, replace what you currently have with this:
import type { Component } from 'solid-js' const App: Component = () => { return ( <div class="container mt-5 text-center"> <h1 class="mb-4">Whattodo!</h1> <form class="mb-5 row row-cols-2 justify-content-center"> <input type="text" class="input-group-text p-1 w-25" placeholder="Add task here..." id="taskInput" required /> <button class="btn btn-primary ms-3 w-auto" type="submit"> Add task </button> </form> <div> <h4 class="text-muted mb-4">Tasks</h4> <div class="row row-cols-3 mb-3 justify-content-center"> <button class="btn btn-danger w-auto">X</button> <div class="bg-light p-2 mx-2">Push code to GitHub</div> <input type="checkbox" role="button" class="form-check-input h-auto px-3" /> </div> </div> </div> ) } export default App
Our JSX code contains the form for inputting new tasks and the tasks section. For now, we’re using hard-coded data, but we’ll learn how we can make our app dynamic so that when a user inputs a new task in the form and clicks the Submit button, our Solid app updates with the new data.
When you go back to your browser, your page should now look like this:
Next, let’s learn how to create and manage state in Solid. We’ll do this by creating a taskList
state and we’ll also create functions for adding new tasks to our state, removing them, and updating their completion status.
Solid has a createSignal
Hook to create state. As an example, let’s create a taskList
state to house our tasks. Inside the ./src/App.tsx
file, we’ll start by creating a type for each task:
const App: Component = () => { type Task = { text: string text: string completed: boolean } return (...) }
Next, we’ll create our taskList
state:
import { Component, createSignal } from 'solid-js' ... const [taskList, setTaskList] = createSignal([] as Task[]) ...
The createSignal()
Hook returns an array containing two variables, taskList
and setTaskList
. Unlike what you’ll see with React Hooks, both variables are functions. We call the taskList()
function to access our task data, and the setTaskList()
function to update our taskList
state.
Now that we’ve created our taskList
state, let’s create a function for adding tasks to our state. We’ll name it addTask
:
const [taskList, setTaskList] = createSignal([] as Task[]) const addTask = (e: Event) => { e.preventDefault() const taskInput = document.querySelector('#taskInput') as HTMLInputElement const newTask: Task = { id: Math.random().toString(36).substring(2), text: taskInput.value, completed: false, } setTaskList([newTask, ...taskList()]) taskInput.value = '' }
Inside of our addTask()
function, we’ve started by using the e.preventDefault()
method to prevent the default reload behavior when we submit our form. We’re also getting our taskInput
from the <input />
element with an ID of “taskInput”.
For each new task, we create an object named newTask
with properties id
, text
, and completed
. When a new task is created, our function will use the Math.random()
method to generate a random string for our task ID and set the default completed
value to false
.
Finally, the setTaskList()
function will be called with an array as its argument, appending the newTask
with the current taskList
state.
Let’s also create a function for deleting tasks:
... const deleteTask = (taskId: string) => { const newTaskList = taskList().filter((task) => task.id !== taskId) setTaskList(newTaskList) }
When we call our deleteTask()
function with the task ID as its argument, it will filter through our taskList
state and return every task except the one with the ID we want to delete. Then, the setTaskList()
method will be called with the new task list as its argument.
To put our addTask()
function to use, we’ll add an onSubmit
event listener to our <form />
tag in the JSX code, which will call our function whenever the submit button is clicked.
... return ( <div class="container mt-5 text-center"> <h1 class="mb-4">Whattodo!</h1> <form class="mb-5 row row-cols-2 justify-content-center" onSubmit={(e) => addTask(e)}> ... </form> </div> )
Next let’s see how we can show our taskList
data in our app whenever a user adds a new task.
Solid has a <For />
component for looping through data. While the JavaScript Array.map()
method will work, our component will always map the taskList
state when it’s updated. With the <For />
component, our app will only update the exact part of the DOM that needs updating.
Let’s replace what we currently have in the Tasks div
with this:
... <div> <h4 class="text-muted mb-4">Tasks</h4> <For each={taskList()}> {(task: Task) => ( <div class="row row-cols-3 mb-3 justify-content-center"> <button class="btn btn-danger w-auto">X</button> <div class="bg-light p-2 mx-2">{task.text}</div> <input type="checkbox" checked={task.completed} role="button" class="form-check-input h-auto px-3" /> </div> )} </For> </div> ...
Notice how we’re wrapping our taskList
in the <For />
component. We’ve also updated the task text from “Push code to GitHub” to task.text
from our task
parameter.
We can now go ahead and use the deleteTask()
method we created earlier. We’ll add an onClick
event listener to the Delete button:
... <button class="btn btn-danger w-auto" onclick={() => deleteTask(task.id)}> X </button> ...
If we go over to our browser, our Solid app should now work like this:
Solid has a createStore()
Hook for creating and managing nested states. But before we talk about it, let’s see how we can make updates to pre-existing tasks in our createSignal()
state. We’ll create a new function named toggleStatus
just under the deleteTask()
function:
... const toggleStatus = (taskId: string) => { const newTaskList = taskList().map((task) => { if (task.id === taskId) { return { ...task, completed: !task.completed } } return task }) setTaskList(newTaskList) }
Our toggleStatus()
function expects a taskId
argument, which we’ll use to get the particular task we want to mark as either completed or not-completed. We’re also using the map()
method to loop through our taskList
state, and if we find the task that has the same ID as the parameter taskId
, we’ll change its completed
property to the opposite of what’s currently there. So, if true
, we’ll make it false
, and if false
, true
.
Finally, we’re using the setTaskList()
method to update the taskList
state with our new taskList
data.
Before we use our toggleStatus()
function, let’s add a distinction between completed tasks and uncompleted tasks in our JSX code. We’ll add the Bootstrap class “text-decoration-line-through text-success” to the task text if its completed
property is true
. In our JSX code, just below the Delete button, let’s update the task text div
to this:
<div class={`bg-light p-2 mx-2 ${task.completed && 'text-decoration-line-through text-success'}`}> {task.text} </div>
Next, we’ll add an onClick
event listener to the checkbox input tag, where we’ll call the toggleStatus()
method whenever it’s clicked:
<input type="checkbox" checked={task.completed} role="button" class="form-check-input h-auto px-3" onClick={() => { toggleStatus(task.id) }} />
The JSX code that our <App />
component returns should now look like this:
<div class="container mt-5 text-center"> <h1 class="mb-4">Whattodo!</h1> <form class="mb-5 row row-cols-2 justify-content-center" onSubmit={(e) => addTask(e)}> <input type="text" class="input-group-text p-1 w-25" placeholder="Add task here..." id="taskInput" required /> <button class="btn btn-primary ms-3 w-auto" type="submit"> Add task </button> </form> <div> <h4 class="text-muted mb-4">Tasks</h4> <For each={taskList()}> {(task: Task) => ( <div class="row row-cols-3 mb-3 justify-content-center"> <button class="btn btn-danger w-auto" onclick={() => deleteTask(task.id)}> X </button> <div class={`bg-light p-2 mx-2 ${task.completed && 'text-decoration-line-through text-success'}`}> {task.text} </div> <input type="checkbox" checked={task.completed} role="button" class="form-check-input h-auto px-3" onClick={() => { toggleStatus(task.id) }} /> </div> )} </For> </div> </div>
When we go over to our browser, our Solid app should be able to work like this:
createStore
for nested reactivity in SolidBefore we wrap up, let’s see how we can use the createStore
Hook in Solid to create and update nested state. Instead of mapping through our state, creating a new task list, and replacing all of our state data with the new list, we can instead directly update the task that needs updating using its ID.
To use the createStore
Hook, we’ll first import it from solid-js/store
:
import { createStore } from 'solid-js/store'
Notice that createSignal
was imported from solid-js
, while createStore
is imported from solid-js/store
.
Next, we’ll update our taskList
state creation to this:
const [taskList, setTaskList] = createStore([] as Task[])
The store we create with the createStore()
Hook is not a function, unlike the one created with the createSignal()
Hook. So, we’ll modify all instances of taskList
in our code to just taskList
instead of taskList()
. The second variable, setTaskList
, remains a function, and we’ll use it to update our store.
Let’s go ahead and use the setTaskList()
method to modify the toggleStatus()
function:
const toggleStatus = (taskId: string) => { setTaskList( (task) => task.id === taskId, 'completed', (completed) => !completed, ) }
In the toggleStatus()
function, we pass three arguments to the setTaskList()
method:
taskId
parametercompleted
When we go back to the browser, our app should still work as expected:
In this article, we’ve covered the basics of Solid by building a task tracker. Solid’s approach to building web applications is quite impressive and relatively straightforward when compared to other frontend frameworks like Angular and React. With direct compilation to real DOM nodes and without the need for a virtual DOM, web applications built with Solid have the uncommon advantage of being fast.
That said, Solid is still new, and its ecosystem and community are small compared to that of frameworks like React, Vue, and Angular, so there’s a good chance that you’ll be the first to encounter problems or be in need of specific functionalities, libraries, or integrations.
But Solid is growing fast and lots of people have already started migrating existing apps to the framework. The Solid community is fairly responsive and you should not have any problem getting help when you need it.
You can find the repo for our task tracker on my GitHub. I also have a Solid crash course on my YouTube channel, which I’d love for you to check out. And if you want to keep in touch, consider following me on LinkedIn. Keep building!
LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.
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.