DEV Community

Cover image for Let's learn Solid.js quickly, by creating a useDebounce hook
aderchox
aderchox

Posted on

Let's learn Solid.js quickly, by creating a useDebounce hook

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!):

function Likes() {
  const [likeCount, setLikeCount] = createSignal(0);
  return (
    <button
      onClick={function like() {
        setLikeCount(likeCount() + 1);
      }}
    >
      {likeCount()} likes
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

(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() } Note: it has to be wrapped in curly braces due to how JSX works.). 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:

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"));
Enter fullscreen mode Exit fullscreen mode

(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".

createEffect(() => {
  console.log(`I Was Liked ${likeCount()} times, World!`);
});
Enter fullscreen mode Exit fullscreen mode

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

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:

  createEffect(on(likeCount, (c) => {
    console.log(`I Was Liked ${c} times, World!`);
  }, { defer: true }));
Enter fullscreen mode Exit fullscreen mode

(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

The useDebounce hook

Now back to our initial goal at hand! This is an example of how we want to use this hook:

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).
Enter fullscreen mode Exit fullscreen mode

Let's just code it up and hopefully by now, it will be talking to you in a more familiar language:

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;
}
Enter fullscreen mode Exit fullscreen mode

(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!

  const [likeCount, setLikeCount] = createSignal(0, { equals: false });
Enter fullscreen mode Exit fullscreen mode

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!

function useDebounce(signalSetter, delay) {
  let timerHandle;
  function debouncedSignalSetter(value) {
    clearTimeout(timerHandle);
    timerHandle = setTimeout(() => signalSetter(value), delay);
  }
  onCleanup(() => clearInterval(timerHandle));
  return debouncedSignalSetter;
}
Enter fullscreen mode Exit fullscreen mode

(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

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 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" for suggesting improvements, and to the amazing folks on the Solid's Discord server 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.

Oldest comments (2)

Collapse
 
lexlohr profile image
Alex Lohr

Thanks for this article and for mentioning solid-primitives.

Collapse
 
aderchox profile image
aderchox

My pleasure, thanks for that amazing library and all contributions πŸ™‚πŸ‘