Skip to content

Conversation

@sp90
Copy link

@sp90 sp90 commented Feb 20, 2025

Description

Currently it takes quite a bit of code to implement debounces in signal land if you wanna move towards becoming undependant on rxjs so the idea is to have a signal primitive that has debounce built in

This approach has an important drawback:

It uses a lot of the internals of the signal implementation that could break should the angular team decide to change the base implementation of signals. Then this would need to be updated as well

Proposed API

debounceSignal<T>(
    initialValue: T,
    time: number,
    options?: CreateSignalOptions<T>,
): WritableSignal<T>

Usage example

// in component
example = debounceSignal('', 300)

ngOnInit() {
  this.example.set('hello') // 300ms after this are reacted to on all listeners
}

// in markup
<input type="text" [(ngModel)]="example" />

<pre>{{ example() }}</pre>

Worth noting it also works with all data types ex string, number, array, object also nested values i wanna emphasise this test fx for nested objects

const initialObject = { a: 1, b: { c: 2 } };
const s = debounceSignal(initialObject, 300);

expect(s().b.c).toEqual(2);

s.update((val) => ({ ...val, b: { ...val.b, c: 3 } }));
s.update((val) => ({ ...val, b: { ...val.b, c: 4 } }));
s.update((val) => ({ ...val, b: { ...val.b, c: 5 } }));

expect(s().b.c).toEqual(2);

jest.advanceTimersByTime(301);
expect(s().b.c).toEqual(5);

Benefits:

More intuitive API for when having to debounce values
Clearer code intention
Easier to understand

@sp90
Copy link
Author

sp90 commented Feb 21, 2025

While the Alex and Pawal have valid points I still believe it would be a good alternative for the time being until we have some better ways of doing it inside angular or a better interface surface in angular

https://x.com/SimonBitwise/status/1892573669471215932

@nartc
Copy link
Collaborator

nartc commented Mar 11, 2025

Hi, thank you for the PR. At the moment, I'll not be accepting debounceSignal because of current debate surrounding this issue. I'll keep the PR opened until we see things settle a bit to either close or merge.

Personally, I think signal should not be debounced, ever. That's my personal stance (and why I feel strongly about this)

@nartc nartc self-assigned this Mar 11, 2025
@nartc nartc added the DO NOT MERGE Do not merge this PR for any reason label Mar 11, 2025
@sp90
Copy link
Author

sp90 commented Mar 11, 2025

I was procrastinating on the subject after the debate that I started on X surrounding the matter

Where I think I found some good reasons for why I could be useful let me know what you think

https://x.com/SimonBitwise/status/1893046236862967885

@YeisonHerrer
Copy link

Hi 👋 If debounceSignal isn't the preferred approach due to being a writable signal that affects the original state, how about considering an alternative like debounceComputed?

It's a derived signal based on linkedSignal that applies debounce without modifying the source. Developers can use it when needed, like for stabilizing values before passing them to something like httpResource.

Here's a possible implementation:

import { linkedSignal, type ValueEqualityFn } from '@angular/core';

interface DebounceComputedOptions<S> {
  equal?: ValueEqualityFn<NoInfer<S>>;
  computation: () => S;
  time: number;
}

export function debounceComputed<S>({ computation, time, equal }: DebounceComputedOptions<S>) {
  let timeoutId: null | ReturnType<typeof setTimeout> = null;

  const debounced = linkedSignal<S, S>({
    equal,
    source: computation,
    computation: (source, previous) => {
      if (timeoutId) clearTimeout(timeoutId);
      
      timeoutId = setTimeout(() => debounced.set(source), time);
      
      return previous?.value ?? source;
    }
  });

  return debounced.asReadonly();
}

Just wanted to share another way to approach this use case

@sp90
Copy link
Author

sp90 commented May 30, 2025

So the original reason was I raised the question on X and then some of the devs from the angular team rather wanted the debounce at the event source like on the ngModel, on the addEventlistener etc

You're approach could also be an option what my implementation basically do are something similar to a computed as it creates an internal signal that are modified after the debounce

So it is still a derived state that can be modified if you wanted to limit that you could just return that signal asReadonly()

@max-scopp
Copy link

See #595

I think some way to defering inputs is really helpful, especially when you want a simple, yet reactive search input.
It feels really clunky and unintuitive to have a signal, make it an observable and then back to a signal for usage such as httpResource.

I'm sure if angular ever drop observables, people will find an alternative way like I did with using libraries such as throttle-debounce in react previously.

In the end, I think it always boils down to having a signal, wether or not the inputs/setters/updaters of that signal is being deferred, can be argued.

As response, I would perhaps suggest a readonly signal where you do not set or update values, but rather imply the internal Subject with a next function: #603

@sp90
Copy link
Author

sp90 commented Nov 7, 2025

@max-scopp Now i can't find it but I saw a WIP implementation of a debounce helper to use inside the signal forms callback so you have a debounce thing in there instead of on the signal type but as with a lot of the other things angular currently very experimental and might go away in the future

@sysmat
Copy link

sysmat commented Nov 8, 2025

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

DO NOT MERGE Do not merge this PR for any reason

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants