Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add effectOnceIf helper function #419

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

lorenzodianni
Copy link

I'd like to propose this helper function, I'm open to implementing improvements and making changes as needed 馃檶
I'm not sure about the name, so suggestions are appreciated 馃

effectOnceIf

It is an helper function that allows you to create an effect that will be executed only once if a certain condition occurs.

Usage

@Component({})
class Example {
  count = signal(0);

  effectOnceIfRef = effectOnceIf(
    // condition function: if it returns a truly value, the execution function will run
    () => this.count() > 3,
    // execution function: will run only once
    (valueReturnedFromCondition, onCleanup) => {
        console.log(`triggered with value returned: ${valueReturnedFromCondition}`);
        onCleanup(() => console.log('cleanup'));
    });
}

// example.count.set(1);
//    -> nothing happens
// example.count.set(4);
//    -> log: triggered with value returned: true
//    -> log: cleanup
// example.count.set(6);
//    -> nothing happens

@eneajaho
Copy link
Collaborator

Hello @lorenzodianni
I wanted to know more about the use cases that this solves.

@nemu69
Copy link

nemu69 commented Jun 13, 2024

Hi @eneajaho,

Suppose we have a task management application where each task has a progress state that ranges from 0% to 100%. We want to trigger an alert (e.g., display a notification or log a message) when a task's progress exceeds 75% for the first time. Once this alert is triggered, it should not trigger again, even if the task's progress continues to increase.

@Component({})
class TaskProgress {
  // Declare a signal for the task's progress
  progress = signal(0);

  // Use effectOnceIf to trigger an alert when progress exceeds 75%
  effectOnceIfRef = effectOnceIf(
    // Condition: if progress exceeds 75%
    () => this.progress() > 75,
    // Execution function: triggers the alert once
    (valueReturnedFromCondition, onCleanup) => {
      console.log(`Alert: Progress has exceeded 75%! Current value: ${this.progress()}%`);
      onCleanup(() => console.log('Cleanup after alert'));
    });
}

// Update the task's progress
taskProgress.progress.set(50);
//    -> nothing happens
taskProgress.progress.set(80);
//    -> log: Alert: Progress has exceeded 75%! Current value: 80%
//    -> log: Cleanup after alert
taskProgress.progress.set(90);
//    -> nothing happens

Perhaps @lorenzodianni has other use cases.

@lorenzodianni
Copy link
Author

Hi @eneajaho
I frequently use it for initializing data or libraries, DOM manipulation, or any tasks that need to be executed only once:

  • Initialize the library when the DOM is available.
  • Initialize the library when the DOM enters the viewport (using the Intersection Observer).
  • Invoke an API that should be called once (e.g., navigator.getCurrentPosition).

Here are some real use cases:

effectOnceIf(
  () => this.mapRenderer.isMapReady() ? this.markers() : false,
  (markers) => this.mapRenderer.addMarkers(markers),
);
effectOnceIf(
  () => {
    const myDomEl = this.myDomEl();
    return myDomEl && this.canInitializeMyDomLib() ? myDomEl : false;
  },
  (element) => {
    new MyDomLib(element, {...});
  },
);
effectOnceIf(
  () => this.userHasAcceptedGeolocationPermissions() ? this.getCurrentPosition() : false,
  (position) => this.mapRenderer.centerMap(position),
);

And of course, the use case mentioned by @nemu69 could be another one

@joshuamorony
Copy link
Contributor

I think for most cases it might make sense to use a computed signal as the predicate, and a normal effect to run the side effect, e.g:

countGreaterThanThree = computed(() => this.count() > 3);

effect(() => {
  if(this.countGreaterThanThree()){
    // do whatever
  }
})

But I can see the use case where you wouldn't want the effect to run again for situations where the predicate could potentially switch back to being false and then true again.

Perhaps an alternative to consider would be some sort of "trigger" or "tripwire" signal that, once it becomes true, its value can not be changed anymore, e.g something like:

countGreaterThanThree = computedUntilTrue(() => this.count() > 3);

effect(() => {
  if(this.countGreaterThanThree()){
    // do whatever
  }
});

At least to me an API like this feels more idiomatic.

@lorenzodianni
Copy link
Author

Hi @joshuamorony,
I get your point, but from a DX perspective it seems more complicated because the syntax is the same as any other effect/signal. This means we have to trace back to the signal definition to understand that the effect/signal will behave differently from normal:

countGreaterThanThree = computedUntilTrue(() => this.count() > 3);

effect(() => { // <--- a normal effect that should fire on each signal changes, but not in this case
  if(this.countGreaterThanThree()){ // <--- it looks like a regular signal, so I have no information indicating that it will fire only once. To understand its actual behavior, I need to read the signal's definition
    //...
  }
});

That's why I suggested a slightly different syntax, so everything remains scoped and understandable at first glance when reading it:

effectOnceIf( // <--- I can immediately tell it's not a normal effect and understand its actual behavior.
  () => this.count() > 3, // <--- condition
  () => { // <--- execution
    // do whatever
  }
)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants