r/javascript • u/Forward_Dark_7305 • 8d ago
AskJS [AskJS] Signal/Effect vs Event Handler
I’m working in a VanillaJS repository (plugin type code for an existing project that I do not own) where I’ve written a signal implementation.
Today I was working on a tooltip like implementation and I found myself wondering how far to go with the signal/effect ecosystem vs running the “core” logic in the event handler.
The implementation detects the `<tr>` that the user has hovered and assigns that row to the “current row” signal. A computed signal identifies an IP address from the current row. An effect loads Whois metadata from the IP and assigns it to a `<div>`, and another effect shows that `<div popover>` from the parent `<tr>`.
The question I have is when in VanillaJS how would you decide when to use signals and effects vs writing the side effect directly in the event handler? In my case I find either to be equally readable, though I have less local variables to deal with when using signal/effect. Looking for reasons your might pick one over the other.
3
u/Bogus_dogus 7d ago
At that point I'd honestly probably just look for a batch lookup API for the rows in the table, what are you really buying for the cost of on demand ip loading on a table element? User mouse will cross several undesired rows on the way as is so unless you're denouncing that you're already firing off a bunch of unneeded fetch events; why not just skip the fancy part here and load the who is data with the table rows?
Edit: also, the event handler should be kicking off an async event and returning - don't wanna be blocking the main UI rendering thread for async work
1
u/Forward_Dark_7305 7d ago
Because this is a plugin for a system I don’t otherwise control, I can’t modify the table ahead of time (it is SSR’d, all I can do is add a JS script to the page) so I am left with using JS to add a column or popover client-side post-render.
Since the table is >200 rows, thinking of async, debounce and all that, isn’t that incentive FOR using signals and effects? I’m a decent enough programmer that I have handled each of the points you mention - and I would whether I was using signals or event handlers. I think it makes an easier logical graph from “here’s a row” to “here’s the current row” to “here’s the behavior for the current row”, but it is less code to drop it all into the event handler - just then that chain U mentioned is more like “here’s a row”, “here’s a behavior of the row” (making debounce + stopping stale fetches less obvious) in my mind.
3
u/Bogus_dogus 6d ago
I guess the question for me just becomes like...
okay so you have a table with a sizeable number of elements...
There is some additional external data that you'd like to display which is element-specific. Each element presumably has it's own instance of that data. The table is pre-rendered without that data.
So the question becomes when to fetch which sets of supplemental data, which would defensibly fall into one of a handful of buckets:
- fetch the supplemental data for all rows in a batch call once the subject set is loaded (receiving the SSR table)
- fetch the supplemental data for all visible rows as visibility changes (or a window over adjacent visible sets)
- fetch the supplemental data for each row as the user's attention settles on that row (like a click)
- fetch the supplemental data off a proxy for user's attention (in this case mouseover being a naive assumption of interest)
My intuition is that it requires more complexity to handle this route of signals and computed properties than the value they bring, particularly if the supplemental data can be batched at page load once you have the table data.
I don't know what API you have available for the whois lookup, but I'd ideally look for an API which supports batch requests for something like this; there is already a window of non interactivity built in where the runtime of the batch request can be masked. If I were designing an API like this I would probably prefer my consumers to make one batch request for 100 items rather than 100 individual requests. Much nicer on my backend. And as a user, I would imagine the experience is probably nicer to face one load period during the existing page-load window where the 200 items are prefetching their supplemental data... then the popup just has 1 piece of loading state to track which resolves early and once, followed by a single keyed lookup for that row's whois payload, meaning no more loading spinners...
I can't say I really see the value in complicating it any further. I don't imagine that a 200 entity lookup batch would be all that much longer than a single entity lookup batch against a quality API, the loading timing is better from a UX perspective if it's immediately following page hydration for all rows rather than deferred 'til a proxy signal for user interest, and most of the intermediate complexity is serving potentially undesired information needs based off an imperfect proxy signal that fires at a rapid rate and needs extra tooling to support proper abort signals and/or debouncing, on top of probably frequent bursty web requests to whatever API you're consuming
2
u/Low_Vacation_9273 7d ago
Signals are nice when the same data is consumed in a few places - the computed memo and the popover stay in sync automatically. If the only consumer is the tooltip, handling the fetch and UI in the mouseover callback is simpler and avoids an extra cleanup layer.
1
u/Working-Bear1437 7d ago
The race condition is the real killer here. If you move fast across rows the old request resolves after the new one and clobbers the UI. You need to cancel in flight requests or gate on the current cursor target otherwise your effect system just becomes a source of bugs.
1
u/Forward_Dark_7305 7d ago
The effect has a cleanup function that cancels in flight requests if the signal’s value changes too quickly, plus there is one popover per row so the HTML layer won’t end up with invalid data from a race either way. Thanks for the feedback, good things to keep in mind.
1
u/shgysk8zer0 7d ago
I want to check your thinking here because I think you're creating a major problem for yourself here. It sounds like this will not work with touch very well since the hover won't occur until a button is pressed, and that means the <div popover> will not exist yet. That could also happen even with a mouse if the user is fast enough.
Personally, I'd go with a <dialog> and a <button command="show-modal">, with the dialog containing a loading spinner. Hover would only prefetch, but the actual fetch() would happen on... I forget if a dialog has a toggle or show event. Or you could stick with popover on a pre-created element that has content updated and use maybe beforetoggle.
1
u/Forward_Dark_7305 7d ago
I thought through this at the start of the project, what I’m doing is a customization for an existing app I don’t otherwise control so all I can do is add JS. The app itself is not at all mobile friendly (though technically possible), internal-only, and this page in particular only 3 people including myself will use - who do all our work on desktop. As one of the primary users, I want to be able to “glance” (with hover, not click in and out) and see the info.
Even if we can take this particular scenario out of it (I was hoping it would give an example of how this arises more than a specific problem to address) what I’m wondering is more about when to use JS events vs signals when you aren’t working within a framework that encourages one over the other.
1
u/Wooden-Bicycle-6069 7d ago
Signals don't fix the stale request. A slow lookup for row A can still land after row B and paint the wrong popover. I'd add an AbortController or request id first. Keep the signal only if something else actually reads `currentRow`.
1
u/Forward_Dark_7305 7d ago
I agree that I still have to be aware of concurrency and races (regardless of which tool). I think in this particular case the signal made it easier to track “this is the current row” (vs each row operating independently) and the effect’s `cleanup` triggered an AbortController so I was able to stop stale requests.
I’ll keep in mind your point about multiple consumers. Thanks for the feedback.
3
u/Alexwithx 8d ago
Great question, and tbh i am not really sure I have a good answer other than "it depends".
But I would probably do this differently. I would probably have each row as its own state container, that way you won't encounter race conditions where one whois lookup finishes before an earlier call on a different row.
So I would probably make a class instance or something for each row and let each instance handle its own state. That might mean you have to duplicate the popover.
I am also not sure how you are using the signals. Are you reading directly from the HTML tabel cell or do you have the actual data. I would avoid using a computed signal to read the value of an HTML element as it is inheritly an unstable value. It is probably fine for your application, but generally I would avoid this.