Ebenezer Don Full-stack software engineer with a passion for building meaningful products that ease the lives of users.

Build a task tracker with SolidJS and TypeScript

10 min read 2919

Build a task tracker with SolidJS and TypeScript

SolidJS is fast becoming the center of attention in the web development community. With its straightforward state management, fine-grained reactivity, and high performance, SolidJS has put itself on a pedestal for other JavaScript frameworks.

SolidJS is everything React developers have been asking for, and in this article, I’ll walk you through building a task tracker with SolidJS. We’ll cover the following topics:

Prerequisites

To follow along with this tutorial, you’ll need knowledge of JavaScript and TypeScript, Node.js modules, and components in frontend frameworks.

Why SolidJS?

If you’ve worked with React before, SolidJS 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 SolidJS, 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.

SolidJS also uses JSX, the popular HTML-like syntax extension to JavaScript. This makes handling UI logic, events, and state changes straightforward. Coupled with that, SolidJS compiles to plain JavaScript, so there’s no need for a virtual DOM, making it relatively faster than frameworks like React and Angular.

SolidJS 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 SolidJS 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 SolidJS app to learn how to build a web app with SolidJS step-by-step.

Setting up a SolidJS app with TypeScript

To set up a SolidJS 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 SolidJS 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:

View of the generated app structure


More great articles from LogRocket:


Notice that our SolidJS 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 SolidJS 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:

  1. A function that returns our <App /> component
  2. An HTML element

When 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 SolidJS 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:

Edit src/App.tsx and save to reload.

Installing Bootstrap to style our SolidJS app

Now that we’ve successfully set up our SolidJS 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)

Using JSX to structure our task tracker

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 SolidJS app updates with the new data.

When you go back to your browser, your page should now look like this:

SolidJS App in browser

Next, let’s learn how to create and manage state in SolidJS. 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.

Creating and updating state in SolidJS

SolidJS 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.

Adding tasks to our 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.

Control flow and looping through data in SolidJS

SolidJS 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 SolidJS app should now work like this:

Adding tasks by typing in the task bar

Updating tasks status in our nested state

SolidJS 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 SolidJS app should be able to work like this:

Adding tasks, checking them off, and deleting in web app

Using createStore for nested reactivity in SolidJS

Before we wrap up, let’s see how we can use the createStore Hook in SolidJS 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:

  1. A function to get the particular task we want to update. In our case we’re returning the task that has the same id as the taskId parameter
  2. The property we want to modify — completed
  3. For the third argument, we’re passing another function that takes in the current value of our chosen property and returns a new value. Here, we’re returning the opposite of what it currently is

When we go back to the browser, our app should still work as expected:

Task tracker app is still working

Conclusion

In this article, we’ve covered the basics of SolidJS 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 SolidJS have the uncommon advantage of being fast.

That said, SolidJS 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 SolidJS is growing fast and lots of people have already started migrating existing apps to the framework. The SolidJS 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 SolidJS 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!

Writing a lot of TypeScript? Watch the recording of our recent TypeScript meetup to learn about writing more readable code.

TypeScript brings type safety to JavaScript. There can be a tension between type safety and readable code. Watch the recording for a deep dive on some new features of TypeScript 4.4.

Ebenezer Don Full-stack software engineer with a passion for building meaningful products that ease the lives of users.

Leave a Reply