Let's learn Solid.js quickly, by creating a useDebounce hook
Tags: solidjs, javascript, webdev, beginners
Published: 2023-03-02 15:26:08

#### Disclaimer: I'm not a Solid.js affiliate in any ways, and also I'm not experienced enough with Solid.js (~2days!) to claim what I write here is "good solid.js code" or even "correct".

## Introduction
Although I started learning **Solid.js** just two days ago, I feel it's made me very productive. One thing I tried to do with it as a  first personal exercise, was creating a "drawer" (drawers are those menus that enter the page from one side, in case you didn't know).

I wrote just a few lines of very understandable (read "lovely") code, and then voila, it worked!

At this point, I felt very happy with the journey I'd begun, so I decided to do a second exercise: **To make the drawer "less crazy"! I.e., I didn't want it to expand "just when the mouse started hovering on it", I wanted it to "wait" for some time (a delay) and let the user make up their mind whether they actually wanted the drawer to open or not. So I needed a debouncer!** (Don't worry if you don't know what it is, I'll explain).

It happened to be an interesting exercise in the end, and it taught me more of Solid's API, so I decided to share it with other peer learners (you!) too. Don't worry if you don't know any Solid.js, I'll try to explain things bit by bit.

## What is Debouncing?
To me, debouncing means **"not jumping into conclusions"**. A practical example of when a debouncer can be useful is, when the user is typing letters into a search box, and you want to "trigger the search as the user types", you think this would be great UX, the user doesn't have to click/tap the magnifier (search) button, however, there's an issue with this great idea, EVERY letter that the user types, will make a call to your backend..., which is wasteful. Actually, it could even be "bad UX", assume the search box reacting to every letter instantly as the user types, they might feel the search box is made by a crazy developer!

## Solid.js Quick Start Tutorial
### Reactivity
The general idea of UI frameworks is to make views react automatically to changes to "the state of the data" (aka, just "the state"). This makes it way easier to write and reason about UI code, compared to using a bunch of event listeners here and there to "manually" update views once the data has changed.
### Signals
Solid uses the term "signal" for state (data). Well of course that might not be a precise definition of signals, but it's good enough for the beginning. Let's continue the talk in code (which is not cheap, hopefully!):
```js
function Likes() {
  const [likeCount, setLikeCount] = createSignal(0);
  return (
    <button
      onClick={function like() {
        setLikeCount(likeCount() + 1);
      }}
    >
      {likeCount()} likes
    </button>
  );
}
```
(playground: https://playground.solidjs.com/anonymous/20a27607-7aed-4c34-9249-e09580131742)

In the example above:

1. This function is a Solid "component", we've called it "Likes" component. Solid knows how to translate this function into a component under the hood. We just need to return some html from the function (which is basically not "normal HTML", it's "JSX", which gets translated to JavaScript by a compiler before creating the DOM, but we don't need to care about that).

2. (Read this if you're coming from React) You don't need to worry about stale closures when using signal setters in Solid, i.e., passing a mapper function (mapping old value to new value) to the `setLikeCount()` instead of passing the actual value wouldn't be considered a best practice, you _could_ do that, but it's not different. This is due to the different model that Solid has towards reactivity, it doesn't re-run the entire component on re-renders, rather, only the points that need to change, do change _atomically_.

3. We can make "signals" for our data using the `createSignal` function, and then use the signals in our JSX, by "calling" them (e.g., `{ likeCount() }` <sup>Note: it has to be wrapped in curly braces due to how JSX works.</sup>). This will make our view "reactive" to the `likeCount`, and whenever this datum is changed, "all things" that have used it (with a call) will also get updated.
I said all "things" on purpose, because the dependent doesn't have to be only JSX, it can even be another JavaScript variable, or an effect (more on this a bit later). For example, let's say we want the background color of the button to go green when the number of likes reaches 3:
```js
import { createSignal } from "solid-js"
import { render } from "solid-js/web";

function Likes() {
  const [likeCount, setLikeCount] = createSignal(0);
  let likesColor = () => likeCount() >= 3 ? "limegreen" : ""; // <-- The value of the likesColor depends on the value of the likeCount signal, and gets updated whenever likeCount changes. likesColor is also known as a "derived signal".

  return (
    <button
      onClick={function like() {
        setLikeCount(likeCount() + 1);
      }}
      style={{ "background-color": likesColor() }}
    >
      {likeCount()} likes
    </button>
  );
}

render(() => <Likes />, document.getElementById("app"));
```
(playground: https://playground.solidjs.com/anonymous/3bfab2a8-14cf-46e2-873e-2b14adf5b48b)
### Effects
Now let's say, whenever the Like button is clicked, we want to shout it out to the world! For such a thing, we need to "track the signal", which is called an "effect".
```js
createEffect(() => {
  console.log(`I Was Liked ${likeCount()} times, World!`);
});
```
(playground: https://playground.solidjs.com/anonymous/afaae217-4c65-45e2-a88b-a1f0fb666053)

* (Read this if you're coming from React) No dependencies arrays have been passed to `createEffect`, it is not a mistake. Manually passing a dependencies array to `createEffect` is not needed (and does not exist) in Solid, because Solid signals are used in the effect by "calling them as functions", it is able to detect its own dependencies automatically. Solid also provides other APIs like `on` for more granular control on effect's dependencies once it's needed (we'll see a forthcoming example of it soon).

Well...... our effect _works_, but it's kind of lame! Because it even shouts on the very first render, i.e., when we haven't been liked at all!

> I Was Liked 0 times, World!

![Funny GIF indicating, Nah, no way. A man shaking his head like it's a ball of meat.](https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExMDBmMjA0YzdjZjRhODY4ZDVlNjE3MTM0MWYzODg0NWEwNmI2MGFjOSZjdD1n/Atc9QCyWLGHgLZhHDp/giphy-downsized.gif)

So let's fix this _major_ issue. To fix it, we need to skip the first trigger of the effect (the function passed to the `createEffect`), and for that, we need to wrap the effect in `on`. This is NOT the main purpose of the `on` function, rather, it's a means of making dependencies of the effect explicit. (Sometimes we have signals in an effect that we don't want changes to them to re-trigger the effect.) But it's not related to our use-case here, what we want to use from it is just the options object that we can pass to it which allows us to skip the first trigger:
```js
  createEffect(on(likeCount, (c) => {
    console.log(`I Was Liked ${c} times, World!`);
  }, { defer: true }));
```
(playground: https://playground.solidjs.com/anonymous/1a15305d-2620-4aad-9b7f-ee1e633c002b)
As you see in the snippet above:

1. The first argument of the `on` function is the signal we want our effect to explicitly depend on.

2. The second argument is the effect itself, however, the signal's VALUE is passed to it (not the signal itself, so we use it by `c` and not by `c()`).

Now it works just the way we expected!

![High Five Lets Go GIF](https://media.giphy.com/media/aMIICKo93H4ZvM8SBV/giphy-downsized.gif)

## The useDebounce hook

Now back to our initial goal at hand! This is an example of how we want to use this hook:
```js
const [isOpen, setIsOpen] = createSignal(false);
const isOpenDebounced = useDebounce(isOpen, 400);
// A 400 milliseconds delay to let the user make up their mind,
// or regret it (usually this value is between 400 to 600).
```

Let's just code it up and hopefully by now, it will be talking to you in a more familiar language: 
```js
function useDebounce(signal, delay) {
  const [debouncedSignal, setDebouncedSignal] = createSignal(signal());
  let timerHandle;
  createEffect(
    on(signal, (s) => {
      timerHandle = setTimeout(() => {
        setDebouncedSignal(s);
      }, delay);
      onCleanup(() => clearTimeout(timerHandle));
    })
  );
  return debouncedSignal;
}
```
(playground: https://playground.solidjs.com/anonymous/7c8c8d82-502b-4a31-ab96-2cb45a6e705a)

In case you don't understand it yet, it's your own fault, re-read everything, just kidding. The gist of it is, we have defined an effect which depends on the signal we want to debounce, so whenever the signal is reset, the effect is re-triggered, and also `onCleanup` will clear the previously set timer.

There's a big downside to the above solution though, the passed state MUST be initiated using the `equals: false` option, so that createEffect gets re-triggered even when the state is set with the same value!
```js
  const [likeCount, setLikeCount] = createSignal(0, { equals: false });
```
Of course not ideal.

## A _much more Solid_ solution!
A better (**much better** 😅) implementation of the useDebounce debounces the "state setter" (not the getter), e.g., the `setLikeCount`, not the `likeCount`. Not only this solution is easier, more readable, and doesn't require setting an `{ equals: false }` option, but also demonstrates how close Solid.js can be to vanilla JavaScript!
```js
function useDebounce(signalSetter, delay) {
  let timerHandle;
  function debouncedSignalSetter(value) {
    clearTimeout(timerHandle);
    timerHandle = setTimeout(() => signalSetter(value), delay);
  }
  onCleanup(() => clearInterval(timerHandle));
  return debouncedSignalSetter;
}
```
(playground: https://playground.solidjs.com/anonymous/0850d269-ca93-4a6d-9607-ca8c2aa2f0cb)

`createEffect` and `on` are not needed anymore!

In case you're wondering what happens to `clearTimeout` in the first render: `clearTimeout`'s API is OK with `undefined` passed to it and just ignores it. Also in case you're wondering why we need that `onCleanup` call, that's because the last time the timer is set, the component might unmount without cleaning up the timer, which could cause an error. (This example also shows how `onCleanup` is not dependent on `createEffect`, which might be against what you're used to, if you're coming from React.)

This is the final result:
![Preview of the final result](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d8rlc09wxj17gw58kx7w.gif)

## Bonus: Solid Primitives Library
Although the above solution _works_, it's not tested enough, and as I initially said, I'm not experienced enough with Solid to guarantee the quality of the above solution. But no worries, thankfully Solid's community is so helpful and they've already provided a library like React's usehooks-ts for such frequent needs, and they've called it "solid-primitives". So if you want a battle-tested solution for debouncing using Solid, I recommend using the [debounce primitive](https://github.com/solidjs-community/solid-primitives/tree/main/packages/scheduled#debounce) from this library. Install, import and use it just the way we used our own `debounce` hook above! Please don't forget to also give them a star if their work makes your life as a developer easier ☺.

Thanks for reading this article, you're very welcome to suggest any fixes and improvements in the comments section below.

Happy Solid coding. 👋

Thanks to my friend "[David Guillaumant-Mergler](https://dev.to/visionspider)" for suggesting improvements, and to the amazing folks on the [Solid's Discord server](https://discord.com/invite/solidjs) for their considerable help and inexhaustible patience.

---------------------------------------
Hey, you're also invited to join our small and fledging 🐥🐣 Discord community called **TIL** (stands for "**Today-I-Learned**") where we share things that we learn with each other, in the realm of web programming. Join using this [link](https://discord.gg/XxzKSgdSAD).