How to create a signal that delays the underlying signal and clears immediately

850 Views Asked by At

I want to make a signal that sets itself to underlying signal or a memo after a certain delay, and clears immediately if the underlying signal is cleared. The following code demonstrates what I want.

import { render } from "solid-js/web";
import { For, createSignal, createEffect } from "solid-js";

function Counter() {
   const [s, setS] = createSignal(undefined)
   const [delayedS, delayedSetS] = createSignal(undefined)


   setTimeout(() => {
     setS(10)
   }, 1000)

   setTimeout(() => {
     setS(undefined)
   }, 3000)

   createEffect(() => {
      let res = s()
      if (res !== undefined) {
         setTimeout(() => delayedSetS(res), 1000)
      } else {
         delayedSetS(undefined)
      }
   })

  return (<>
    <span> S {s()} </span>
    <span> Delayed S {delayedS()}</span>
    </>)
}

render(() => <Counter />, document.getElementById("app"));

This works, though Is this a correct approach. I am not sure if createDeferred provides this functionality, though I don't want to use that, since it uses a scheduler I am not sure what it does.

2

There are 2 best solutions below

0
On BEST ANSWER

Your attempt was totally valid.

The createDeferred, although it seams like it has a similar usecase, it wouldn't work here as intended, because:

  1. The timeout passed to createDeferred is not an exact amount of time that it will wait before causing updates – it is a maximum timeout to wait before browser becomes idle.
  2. The timeout would happen before both updating the signal to undefined and other values.
const delayedS = createDeferred(s)

A preferable primitive for setting signals is createComputed. It is a pure computation – similar to createMemo – meaning that it will run immediately before effects. In simple examples there is usually no difference to use of effect, but using an effect for setting signals could potentially cause more updates than necessary.

  createComputed(
    on(s, (v) => {
      if (v) setTimeout(() => setDelayedS(v), 1000);
      else setDelayedS();
    })
  );

Additionally clearing the timeout on the set to undefined that happens without a delay, could eliminate some timing glitches.

  let timeoutId = 0;
  createComputed(
    on(s, (v) => {
      if (v) timeoutId = setTimeout(() => setDelayedS(v), 1000);
      else {
        setDelayedS();
        clearTimeout(timeoutId);
      }
    })
  );
  onCleanup(() => clearTimeout(timeoutId));
0
On

You can use the @solid-primitives/scheduled package which has some delayed reactive functionality.

import {createMemo, createSignal} from 'solid-js';
import {createScheduled, throttle} from '@solid-primitives/scheduled';

const [count, setCount] = createSignal(0);
const scheduled = createScheduled(callback => throttle(callback, 1000)); // 1000 ms delay

const debouncedCount = createMemo(
  (previousCount) => scheduled() ? count() : previousCount,
  0
);

createEffect(() => {
  // This message gets printed at most once every second because debouncedCount()
  // is a dependency, regardless of how many times setCount is called.
  console.log(debouncedCount());
});

Here, if one were to call setCount repeatedly, debouncedCount will only update every second. There are other scheduling options such as debounce, and modifiers such as leading.

The page above also has a demo that demonstrates the differences between the different scheduling options.