Adding Interactivity with State

In the previous lesson, we learned how to architect a Solid application using components and JSX. We then built a Bookshelf application using our new knowledge, but it's not quite complete. In this lesson, we'll learn how to add state to our app. We'll then apply this knowledge to our Bookshelf application to bring it to life.

A note on primitives

As we proceed through these tutorials, we'll start hearing about Solid primitives. While components are the building blocks of views in Solid apps, primitives are the building blocks of interactivity. The first primitive that we'll be learning about is the signal.

Managing basic state with signals

In Solid, the most basic way to manage state in our application is to use a signal. To create a signal, Solid provides a createSignal function:

tsx
import { createSignal } from "solid-js";
const [count, setCount] = createSignal(0);
tsx
import { createSignal } from "solid-js";
const [count, setCount] = createSignal(0);

There's a lot going on here: first, we call createSignal with the initial value of state. In this case, our count is going to start at 0. The createSignal function returns a two-element array and we use JavaScript destructuring assignment to unpack this array. In this case, we assign the first element to a variable called count and the second element to a variable called setCount.

The first element, count, is an accessor function (also referred to as a getter) that returns the current value of state.

It's important to note that this is a function that gets the current value, rather than the value itself. Along the way, this function call tells Solid to take note that we've accessed the signal here.

tsx
import { createSignal } from "solid-js";
const [count, setCount] = createSignal(0);
console.log(count()); // 0
tsx
import { createSignal } from "solid-js";
const [count, setCount] = createSignal(0);
console.log(count()); // 0

The second element, setCount, is a setter function. If we want to increment our count, we can pass count() + 1 to setCount:

tsx
import { createSignal } from "solid-js";
const [count, setCount] = createSignal(0);
setCount(count() + 1);
console.log(count()); // 1
tsx
import { createSignal } from "solid-js";
const [count, setCount] = createSignal(0);
setCount(count() + 1);
console.log(count()); // 1

Note that, in order to see our count value's new value, we added our console.log statement after we used setCount.

A key to Solid's reactive system is that we don't really have to do this. Instead, we can listen for—and instantly react to—any signal changes by using our next primitive: the effect.

Reacting to changes with effects

The ability to react to signal changes underpins Solid's reactive system. The most basic way to do so is to use an effect. We can create an effect by using the createEffect hook:

tsx
import { createSignal, createEffect } from "solid-js";
const [count, setCount] = createSignal(0);
createEffect(() => {
console.log(count());
});
setCount(count() + 1);
tsx
import { createSignal, createEffect } from "solid-js";
const [count, setCount] = createSignal(0);
createEffect(() => {
console.log(count());
});
setCount(count() + 1);

To use createEffect, we pass it a function. When any signals used in that function update, the function will rerun.

In this example, our effect depends on count and therefore the effect runs whenever count changes. Accordingly, we log 1 to the console just like before.

Automatic effect dependency tracking is made possible by count being a function. When the count function is called inside an effect, that effect is registered as a listener for the signal. This is why it's so important that our signals are functions!

Rendering with signals

Before we get back to our bookshelf, let's see an example of how these primitives can be used inside components.

tsx
import { createSignal } from "solid-js";
function Counter() {
const [count, setCount] = createSignal(0);
return <div>Current count: {count()}</div>;
}
tsx
import { createSignal } from "solid-js";
function Counter() {
const [count, setCount] = createSignal(0);
return <div>Current count: {count()}</div>;
}
Current count: 0

We see that, much like other variables, we can use signals inside our JSX code by including them inside curly braces. This component is not too interesting yet; so let's add the ability to increment our count. We can do this by adding a <button> element and giving it a click handler using the onClick attribute. This click handler will increment our count by using the setCount function:

tsx
import { createSignal } from "solid-js";
function Counter() {
const [count, setCount] = createSignal(0);
const increment = () => {
setCount(count() + 1);
};
return (
<div>
Current count: {count()}
<button onClick={increment}>Increment</button>
</div>
);
}
tsx
import { createSignal } from "solid-js";
function Counter() {
const [count, setCount] = createSignal(0);
const increment = () => {
setCount(count() + 1);
};
return (
<div>
Current count: {count()}
<button onClick={increment}>Increment</button>
</div>
);
}
Current count: 0

And now we have a functioning counter! Notably, our text updates whenever our count is incremented. Does this remind you of an effect? Whenever the signal changes, the code that controls that part of the DOM reruns, similar to how the code in our effect reran whenever count changed.

Behind the scenes, Solid's compiler creates effects based on our JSX. It sees that we're using count() in a specific part of the DOM, and it creates an effect that updates just that part of the DOM when the signal reruns.

A driving philosophy of Solid is that, by treating everything as a signal or an effect, we can better reason about our application.

Revisting the bookshelf

We now have the tools necessary to make our Bookshelf application interactive. As a refresher, here's the current state of the app with the following components:

  • BookList, a list of books on our Bookshelf
  • AddBook, a form that will allow us to add more books to the shelf
  • Bookshelf, our main application component that contains the other two
tsx
import { BookList } from "./BookList";
import { AddBook } from "./AddBook";
function Bookshelf(props) {
return (
<div>
<h1>{props.name}'s Bookshelf</h1>
<BookList />
<AddBook />
</div>
);
}
function App() {
return (
<Bookshelf name="solid"/>
);
}
export default App;
tsx
import { BookList } from "./BookList";
import { AddBook } from "./AddBook";
function Bookshelf(props) {
return (
<div>
<h1>{props.name}'s Bookshelf</h1>
<BookList />
<AddBook />
</div>
);
}
function App() {
return (
<Bookshelf name="solid"/>
);
}
export default App;

As a first step to adding interactivity, let's add a signal that keeps track of our book list. We'll call it books and it will live in the BookList component. Each book will have a title and an author.

tsx
import { createSignal } from "solid-js";
const initialBooks = [
{ title: "Code Complete", author: "Steve McConnell" },
{ title: "The Hobbit", author: "J.R.R. Tolkien" },
{ title: "Living a Feminist Life", author: "Sarah Ahmed" },
];
export function BookList() {
const [books, setBooks] = createSignal(initialBooks);
return (
<ul>
<li>
{books()[0].title}{" "}
<span style={{ "font-style": "italic" }}>({books()[0].author})</span>
</li>
<li>
{books()[1].title}{" "}
<span style={{ "font-style": "italic" }}>({books()[1].author})</span>
</li>
</ul>
);
}
tsx
import { createSignal } from "solid-js";
const initialBooks = [
{ title: "Code Complete", author: "Steve McConnell" },
{ title: "The Hobbit", author: "J.R.R. Tolkien" },
{ title: "Living a Feminist Life", author: "Sarah Ahmed" },
];
export function BookList() {
const [books, setBooks] = createSignal(initialBooks);
return (
<ul>
<li>
{books()[0].title}{" "}
<span style={{ "font-style": "italic" }}>({books()[0].author})</span>
</li>
<li>
{books()[1].title}{" "}
<span style={{ "font-style": "italic" }}>({books()[1].author})</span>
</li>
</ul>
);
}

There are a couple of things to note here:

First, while we had only used createSignal to maintain the value of a number in state thus far, it can manage all kinds of state. In our Bookshelf application, our signal is an array of objects.

Second, we're now using books directly in our JSX code. We call books() to access the signal array, and then access the element at index 0 (zero) of that array in the first list item and the element at index 1 of that array in the second list item. This will work, but it's not flexible: we want to handle a dynamic number of books.

Looping over items

The best way to loop over items in Solid is the <For /> component. The <For /> component has an each prop, to which we can pass our books() array.

tsx
<For each={books()}></For>
tsx
<For each={books()}></For>

Inside the For component, we use a callback function that will be applied to each element in the array. In this instance, we want each book to be rendered inside an <li>.

tsx
<For each={books()}>
{(book) => {
return (
<li>
{book.title} ({book.author})
</li>
);
}}
</For>
tsx
<For each={books()}>
{(book) => {
return (
<li>
{book.title} ({book.author})
</li>
);
}}
</For>

Our BookList component now looks like this:

tsx
import { createSignal, For } from "solid-js";
const initialBooks = [
{ title: "Code Complete", author: "Steve McConnell" },
{ title: "The Hobbit", author: "J.R.R. Tolkien" },
{ title: "Living a Feminist Life", author: "Sarah Ahmed" },
];
export function BookList() {
const [books, setBooks] = createSignal(initialBooks);
return (
<ul>
<For each={books()}>
{(book) => {
return (
<li>
{book.title}{" "}
<span style={{ "font-style": "italic" }}>({book.author})</span>
</li>
);
}}
</For>
</ul>
);
}
tsx
import { createSignal, For } from "solid-js";
const initialBooks = [
{ title: "Code Complete", author: "Steve McConnell" },
{ title: "The Hobbit", author: "J.R.R. Tolkien" },
{ title: "Living a Feminist Life", author: "Sarah Ahmed" },
];
export function BookList() {
const [books, setBooks] = createSignal(initialBooks);
return (
<ul>
<For each={books()}>
{(book) => {
return (
<li>
{book.title}{" "}
<span style={{ "font-style": "italic" }}>({book.author})</span>
</li>
);
}}
</For>
</ul>
);
}

Derived state

Solid makes it easy to track derived state. You can think of derived state as a computation based only on other information you're already tracking in state. In our Bookshelf application, an example of derived state would be the number of books on our list: it's the length of our books array at any point in time.

In Solid, all we have to do to compute derived state is to create a derived signal: a function that relies on another signal:

tsx
const totalBooks = () => books().length;
tsx
const totalBooks = () => books().length;

Now, whenever we call totalBooks(), Solid will register the underlying signal (books) as a dependency, so the computed value will always stay up-to-date.

tsx
import { createSignal, For } from "solid-js";
const initialBooks = [
{ title: "Code Complete", author: "Steve McConnell" },
{ title: "The Hobbit", author: "J.R.R. Tolkien" },
{ title: "Living a Feminist Life", author: "Sarah Ahmed" },
];
export function BookList() {
const [books, setBooks] = createSignal(initialBooks);
const totalBooks = () => books().length;
return (
<>
<h2>My books ({totalBooks()})</h2>
<ul>
<For each={books()}>
{(book) => {
return (
<li>
{book.title}
<span style={{ "font-style": "italic" }}> ({book.author})</span>
</li>
);
}}
</For>
</ul>
</>
);
}
tsx
import { createSignal, For } from "solid-js";
const initialBooks = [
{ title: "Code Complete", author: "Steve McConnell" },
{ title: "The Hobbit", author: "J.R.R. Tolkien" },
{ title: "Living a Feminist Life", author: "Sarah Ahmed" },
];
export function BookList() {
const [books, setBooks] = createSignal(initialBooks);
const totalBooks = () => books().length;
return (
<>
<h2>My books ({totalBooks()})</h2>
<ul>
<For each={books()}>
{(book) => {
return (
<li>
{book.title}
<span style={{ "font-style": "italic" }}> ({book.author})</span>
</li>
);
}}
</For>
</ul>
</>
);
}

Lifting state up

We want to add a book to the list using our AddBook component. There's one problem though: how do we make the setBooks setter available to the AddBooks component?

We know that parents can pass props to children, but how do sibling components pass props to each other? This is a common problem in Solid and the solution is generally to lift state up to a common parent. In this case, our books signal can live in the Bookshelf component. Then, the BookList component can be passed the data from the getter.

Let's start out by lifting our books signal up to Bookshelf and passing its value back down to the BookList component. You can see the changes we have made in both the App.tsx and BookList.tsx files.

tsx
import { createSignal } from "solid-js";
import { BookList } from "./BookList";
import { AddBook } from "./AddBook";
const initialBooks = [
{ title: "Code Complete", author: "Steve McConnell" },
{ title: "The Hobbit", author: "J.R.R. Tolkien" },
{ title: "Living a Feminist Life", author: "Sarah Ahmed" },
];
function Bookshelf(props) {
const [books, setBooks] = createSignal(initialBooks);
return (
<div>
<h1>{props.name}'s Bookshelf</h1>
<BookList books={books()} />
<AddBook />
</div>
);
}
function App() {
return (
<Bookshelf name="solid"/>
);
}
tsx
import { createSignal } from "solid-js";
import { BookList } from "./BookList";
import { AddBook } from "./AddBook";
const initialBooks = [
{ title: "Code Complete", author: "Steve McConnell" },
{ title: "The Hobbit", author: "J.R.R. Tolkien" },
{ title: "Living a Feminist Life", author: "Sarah Ahmed" },
];
function Bookshelf(props) {
const [books, setBooks] = createSignal(initialBooks);
return (
<div>
<h1>{props.name}'s Bookshelf</h1>
<BookList books={books()} />
<AddBook />
</div>
);
}
function App() {
return (
<Bookshelf name="solid"/>
);
}

Our array of books now lives in the Bookshelf component. We then pass books() to the BookList component. We can now access our books within the BookList component by using props.books.

Note

You may have noticed that we called books() when we passed it to the BookList component—this is not a typo! In Solid, it's a best practice to call a signal accessor when you pass it to a component. In the background, Solid makes this a reactive prop and reactivity will be tracked in the child component's JSX. (TODO: good place to link to a discussion/guide on props and reactivity).

Adding books to the list

Now that we have lifted state, we can add some books to the list. Let's pass our setter to the AddBook component and call setBooks when we click the Add Book button. You can see these changes in the App.tsx and AddBook.tsx files:

tsx
import { createSignal } from "solid-js";
import { BookList } from "./BookList";
import { AddBook } from "./AddBook";
const initialBooks = [
{ title: "Code Complete", author: "Steve McConnell" },
{ title: "The Hobbit", author: "J.R.R. Tolkien" },
{ title: "Living a Feminist Life", author: "Sarah Ahmed" },
];
function Bookshelf(props) {
const [books, setBooks] = createSignal(initialBooks);
return (
<div>
<h1>{props.name}'s Bookshelf</h1>
<BookList books={books()} />
<AddBook setBooks={setBooks} />
</div>
);
}
function App() {
return (
<Bookshelf name="Solid"/>
);
}
export default App;
tsx
import { createSignal } from "solid-js";
import { BookList } from "./BookList";
import { AddBook } from "./AddBook";
const initialBooks = [
{ title: "Code Complete", author: "Steve McConnell" },
{ title: "The Hobbit", author: "J.R.R. Tolkien" },
{ title: "Living a Feminist Life", author: "Sarah Ahmed" },
];
function Bookshelf(props) {
const [books, setBooks] = createSignal(initialBooks);
return (
<div>
<h1>{props.name}'s Bookshelf</h1>
<BookList books={books()} />
<AddBook setBooks={setBooks} />
</div>
);
}
function App() {
return (
<Bookshelf name="Solid"/>
);
}
export default App;

Inside AddBook, we created a function called addBook that is used as the click handler for our form's button. Since we're submitting a real HTML form, we use event.preventDefault() to prevent the default form behavior of executing a post request. Next, we call props.setBooks, but we don't quite know what to pass to our setter.

We know we want to keep the existing books on the list and then add a new book that comes from our form input. To get the existing books, we could use two different approaches: we could pass the books signal down to our AddBook component. While that would work, it's worth exploring the second option: using the callback function form of the setter. We haven't used this yet, and the syntax is as follows:

tsx
setCount((currentCount) => {
return currentCount + 1;
});
tsx
setCount((currentCount) => {
return currentCount + 1;
});

By using this form, our setter has access to the current value of the signal.

This form for our setBooks function solves the first problem: our addBook function can be written as follows:

jsx
const addBook = (event) => {
event.preventDefault();
props.setBooks((books) => {
return books;
});
};
jsx
const addBook = (event) => {
event.preventDefault();
props.setBooks((books) => {
return books;
});
};

Now, we need to append the text from our form inputs to this list. To do so, we can create a new signal inside the AddBook component to track the value of the inputs. We'll make sure this signal is always equal to the inputs' values by using its onInput handler. Additionally, we'll bind the newBook() to the value attribute of our input to make sure our input always reflects the value of the signal.

Finally, we want to add the newBook to our books list and then clear the input field in case our user has more books to enter.

tsx
import { createSignal } from "solid-js";
const emptyBook = { title: "", author: "" };
export function AddBook(props) {
const [newBook, setNewBook] = createSignal(emptyBook);
const addBook = (event) => {
event.preventDefault();
props.setBooks((books) => [...books, newBook()]);
setNewBook(emptyBook);
};
return (
<form>
<div>
<label for="title">Book name</label>
<input
id="title"
value={newBook().title}
onInput={(e) => {
setNewBook({ ...newBook(), title: e.currentTarget.value });
}}
/>
</div>
<div>
<label for="author">Author</label>
<input
id="author"
value={newBook().author}
onInput={(e) => {
setNewBook({ ...newBook(), author: e.currentTarget.value });
}}
/>
</div>
<button type="submit" onClick={addBook}>
Add book
</button>
</form>
);
}
tsx
import { createSignal } from "solid-js";
const emptyBook = { title: "", author: "" };
export function AddBook(props) {
const [newBook, setNewBook] = createSignal(emptyBook);
const addBook = (event) => {
event.preventDefault();
props.setBooks((books) => [...books, newBook()]);
setNewBook(emptyBook);
};
return (
<form>
<div>
<label for="title">Book name</label>
<input
id="title"
value={newBook().title}
onInput={(e) => {
setNewBook({ ...newBook(), title: e.currentTarget.value });
}}
/>
</div>
<div>
<label for="author">Author</label>
<input
id="author"
value={newBook().author}
onInput={(e) => {
setNewBook({ ...newBook(), author: e.currentTarget.value });
}}
/>
</div>
<button type="submit" onClick={addBook}>
Add book
</button>
</form>
);
}

Note

We used the spread operator to create an new books array inside our books setter. This is a common pattern in Solid and helps to make sure we create a new array rather than update (or mutate) the existing signal array. By default, Solid uses referential equality checks when determining if a signal has updated.

Test-driving our app

We now have a dynamic Bookshelf application! Try it out yourself: you should be able to add books using the AddBook component and see those books added to the list in the BookList component.

Solid's Bookshelf

My books (3)

  • Code Complete (Steve McConnell)
  • The Hobbit (J.R.R. Tolkien)
  • Living a Feminist Life (Sarah Ahmed)