remix icon indicating copy to clipboard operation
remix copied to clipboard

change argument order for on function with AbortSignal

Open kentcdodds opened this issue 1 month ago • 6 comments

It's easier to make a PR about this than an issue/discussion. Just curious what you think about doing this to avoid unfortunate function overloading. Overloading is irritating as a consumer of the types. Having an optional options argument at the end seems reasonable.

kentcdodds avatar Nov 03 '25 19:11 kentcdodds

I actually had a similar thought about the 2nd signal arg. In addition to eliminating the function overload, this API would provide a convenient spot to put other addEventListener options, so you could do stuff like this:

on(button, { keydown }, { capture: true, once: true })

// or even
on(button, { keydown }, true) // useCapture

The 3rd arg would essentially be the exact same as the 3rd arg to addEventListener, either options or a useCapture boolean, which makes on feel like a little more like an enhancement on top of addEventListener than a replacement.

mjackson avatar Nov 03 '25 23:11 mjackson

The API is mostly in service to creating elements with JSX. How would you do this?

<div
  on={{
    click: capture(() => {
      // ...
    }),
    focus: () => {
      // not captured
    },
  }}
/>

ryanflorence avatar Nov 04 '25 00:11 ryanflorence

The reason the signal is the optional second arg is just because it's butt ugly with prettier when it's the last arg

```tsx
function ComponentWithWindowEvents(this: Remix.Handle) {
  on(window, this.signal, {
    scroll: () => {},
    focus: capture(() => {}),
    beforeunload: () => {},
  })

  return () => {
    // ...
  }
}

function ComponentWithWindowEvents(this: Remix.Handle) {
  on(
    window,
    {
      scroll: () => {},
      focus: capture(() => {}),
      beforeunload: () => {},
    },
    { signal: this.signal },
  )

  return () => {
    // ...
  }
}

While I originally was thinking of this as a thin wrapper over addEventListener it's more about a declarative way to add multiple events to a single target with different options all at once and to be able to clean them up all up all at once as well.

ryanflorence avatar Nov 04 '25 00:11 ryanflorence

For what it's worth, I had the same thought when reading the docs yesterday, but thought I might just be missing something.

If the main reason is because of the prettier formatting ugliness, I personally don't think that's enough of a reason to not make the signal the 3rd argument.

I also don't mind how it reads that badly, and if I really did care that much I'd just curry it and call onWindow 😉

onWindow(
  {
    scroll: () => {},
    focus: capture(() => {}),
    beforeunload: () => {},
  },
  { signal: this.signal },
)

Either way, a 2nd argument being the optional argument feels really weird on top of it deviating a bit in structure from what devs will be used to with addEventListener. Even if it's not necessarily a wrapper of addEventListener, I think maintaining similar argument order is nice.

brookslybrand avatar Nov 04 '25 17:11 brookslybrand

The concern of this PR would be obviated if there was a this.on() method on Remix.Handle. I haven't seen this idea discussed anywhere.

Essentially, this.signal would be added automatically.

this.on(target, { ...listeners })

As far as allowing AddEventListenerOptions as a third argument, I say “why not?” – No reason not to support both ways of injecting listener options, since the capture()/listenWith() functions are still useful with JSX, as Ryan pointed out.

aleclarson avatar Nov 04 '25 17:11 aleclarson

The concern of this PR would be obviated if there was a this.on() method on Remix.Handle.

I agree with @aleclarson. Ryan and I have discussed a potential this.on API, but haven't finalized anything yet.

I do like the conceptual symmetry between this.on and <button on>. Both this (Remix element) and <button> (DOM element) have a "disconnected" event that we can plug into to automatically remove listeners.

mjackson avatar Nov 04 '25 20:11 mjackson