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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for .once() API #136

Open
0xTheProDev opened this issue Jun 1, 2021 · 19 comments
Open

Support for .once() API #136

0xTheProDev opened this issue Jun 1, 2021 · 19 comments

Comments

@0xTheProDev
Copy link

Motivation
I believe this is quite common use-case where you have to listen for an even only once (for example, module getting ready). The library does not support this inherently but could easily be done with some wiring. This would open up a new use-case support as well as users of the library does not have to write boilerplates on their own.

Example Usage

const emitter: mitt.Emitter = new mitt();

emitter.once('ready', () => console.log("Called Once");

emitter.emit('ready'); // Log: Called Once
emitter.emit('ready'); // No side effect

@developit I would love to know your opinion on this.

@developit
Copy link
Owner

developit commented Jun 23, 2021

I'm not entirely against adding once(). Whether it's worthwhile depends on the size impact. Here is a polyfill:

function mittWithOnce(all) {
  const inst = mitt(all);
  inst.once = (type, fn) => {
    inst.on(type, fn);
    inst.on(type, inst.off.bind(inst, type, fn));
  };
  return inst;
}

The problem with this, and with all of the implementations I have seen including the above two, is that once() and off() can lead to incorrect/unexpected results when used in combination:

const { once, off, emit } = mitt();

on('foo', foo);  // register a normal handler
once('foo', foo);  // ... and a "once" handler

off('foo', foo);  // question: which does this remove? the "once" handler, or the normal handler?

emit('foo');  // correct - in either case we see one foo() invocation here

emit('foo');  // ... but whether this invokes foo() depends on which handler got removed

@0xTheProDev
Copy link
Author

Hmm, that's an interesting use case. And what comes to my mind is either not having once for handler that are already register, or define an order of precedence to remove once and then on. Either way, the behaviour gets defined but a bit opinionated. What do you think?

@developit
Copy link
Owner

It looks like Node just punts on this - it removes the first listener, regardless of whether it was added via once() or on(). On the web, EventTarget supports a {once:true} option, but EventTarget already silently drops duplicate handlers, so this isn't an issue:

const emitter = new EventTarget();
function foo() {}

emitter.addEventListener('foo', foo);  // adds a listener
emitter.addEventListener('foo', foo, { once: true });  // simply ignored

emitter.removeEventListener('foo', foo); // removes the first (only) listener

emitter.dispatchEvent(new Event('foo')); // no listeners registered

emitter.addEventListener('foo', foo, { once: true });  // adds a "once" listener
emitter.addEventListener('foo', foo);  // ignored (treated as duplicate)

emitter.dispatchEvent(new Event('foo')); // invokes foo(), removes the listener

@0xTheProDev
Copy link
Author

Hmm, the web APIs makes sense. Essentially it took the first option that I mention. We can take that as standard. For Node, the behaviour of deletion is then just one directional.

@jacob-indieocean
Copy link

I would love to see once implemented.

But doesn't the polyfill you have above leak? The call to once adds two handlers, but only one of them is removed when the event fires.

@ferrykranenburgcw
Copy link

once is also needed here, hopefully implemented soon

@juliovedovatto
Copy link

juliovedovatto commented Nov 24, 2021

My Two cents in this topic:

const emitter = mitt()

 emitter.once = (type, handler) {
    const fn = (...args) => {
      emitter.off(type, fn)
      handler(args)
    }

    emitter.on(type, fn)
  }
}

export default emitter

I used this approach a few months ago on a medium-sized project and afaik it is working until now, no problems using once this way.

We've even created a unit test to make sure it works. Maybe I missed a specific test or two 🤔

@developit
Copy link
Owner

@juliovedovatto that's how I generally implement this, yep. The reason that solution wouldn't work in Mitt itself is because it becomes impossible to remove a handler added via once() using emitter.off(type, handler).

@sealice
Copy link
Contributor

sealice commented Feb 8, 2022

@juliovedovatto that's how I generally implement this, yep. The reason that solution wouldn't work in Mitt itself is because it becomes impossible to remove a handler added via once() using emitter.off(type, handler).

@developit Can we return a new handler in once() or add the new handler as a property to the handler, so we can use emitter.off(type, handler) to remove the new handler added by once()

emitter.once = (type, handler) {
    const fn = (arg) => {
        emitter.off(type, fn);
        handler(arg);
    };

    emitter.on(type, fn);

    // add a property to the handler
    handler._ = fn;

    // or

    // return this handler
    return fn;
}

This makes it possible to remove handlers added via once() using emitter.off(type, handler).

There is no need to consider how to remove the handlers added by on and once, it is entirely up to the user to decide.

@zjcwill
Copy link

zjcwill commented Aug 24, 2022

Typescirpt version

import mitt, { Emitter, EventHandlerMap, EventType, Handler } from 'mitt';

export function mittWithOnce<Events extends Record<EventType, unknown>>(all?: EventHandlerMap<Events>) {
  const inst = mitt(all);
  inst.once = (type, fn) => {
    inst.on(type, fn);
    inst.on(type, inst.off.bind(inst, type, fn));
  };
  return inst as unknown as {
    once<Key extends keyof Events>(type: Key, handler: Handler<Events[Key]>): void;
  } & Emitter<Events>;
}

@beijinaqie
Copy link

@juliovedovatto
Me too, if you encounter a bug, please let me know, tks

@vascanera
Copy link

Please please please (reiterating this in 2023) add an "official" implementation for .once() to the library. ❤️ Thanks!

@cbloss
Copy link

cbloss commented Sep 14, 2023

I'm asking too! Thanks for the hard work!

@stackoverfloweth
Copy link

this package would also solve your problem
https://github.com/kitbagjs/events

@vascanera
Copy link

Please please please (reiterating this in 2024) add an "official" implementation for .once() to the library. ❤️ Thanks!

@stackoverfloweth
Copy link

Please please please (reiterating this in 2024) add an "official" implementation for .once() to the library. ❤️ Thanks!

I appreciate that you're still holding out hope.

Why not switch to kitbag? It has feature parity, more modern, supports broadcast channel, has your once method, and maybe best of all is actively being maintained.

@leejunhui
Copy link

Please please please (reiterating this in 2024.7.5) add an "official" implementation for .once() to the library. ❤️ Thanks!

@agrohs
Copy link

agrohs commented Oct 3, 2024

Kitbag looks interesting...but looks like it is still a bit early?

Please please please (reiterating this in 2024) add an "official" implementation for .once() to the library. ❤️ Thanks!

I appreciate that you're still holding out hope.

Why not switch to kitbag? It has feature parity, more modern, supports broadcast channel, has your once method, and maybe best of all is actively being maintained.

Screenshot 2024-10-03 at 10 20 45 AM

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

No branches or pull requests