Closed dead-claudia closed 1 month ago
Do you mean, as opposed to just having it for state? I think @shaylew and @modderme123 had some thoughts about how it could relate to async signals.
@littledan Yes, that was the idea.
There are definitely async-related reasons to want these on States, but if there was an async-related reason we needed them on Computeds I'm blanking on that one. (It's possible @modderme123 had one?)
The case that comes to mind for me is for caching of computeds for derived state, where you want to get sharing where possible but don't want to leak computeds that could otherwise be collected. For example, if you have a map where equality comparison of values is potentially expensive, you might want something like this to share the equality comparison work by sharing the selector computeds:
// (I neither ran nor typechecked this code but I think it's coherent?)
function selectFrom<K, V>(map: Signal<Map<K, V>>, options: ComputedOptions = {}): (k: K) => Computed<V> {
const table = new Map<K, Computed<V>>()
return (k: K) => {
return table.get(k) ?? new Signal.Computed(() => map.get().get(k), {
...options,
[Signal.subtle.watched](): { table.set(k, this) },
[Signal.subtle.unwatched](): { table.delete(k) }
})
}
}
There are likely other ways to accomplish this, and yeah the "use a dummy State" construction is sufficient in theory, it just may be unergonomic in practice. So this is more of a "seems like it's probably a good idea" argument than a "this is strictly necessary" one.
@shaylew Wouldn't it be easier to just, in the parent Computed
directly, do map.get().get(k)
?
Like instead of this:
const getKey = selectFrom(someMap)
const selected = getKey("foo")
const useSelected = new Signal.Computed(() => {
doThings(selected.get())
})
Just doing this:
const useSelected = new Signal.Computed(() => {
const selected = someMap.get().get("foo")
doThings(selected)
})
@dead-claudia That makes useSelected
rerun if the map changes but the value at the key in question doesn't, since it performs both reads directly.
@shaylew Fair point. What about this?
const selected = new Signal.Computed(() => someMap.get().get("foo"))
const useSelected = new Signal.Computed(() => {
doThings(selected.get())
})
Just trying to see the point of caching a computed here that's simple enough to where the perf difference is meaningful.
Yeah, that's the same idea as where I was going and where Solid tries to go with createSelector
(although theirs is a slightly different beast is an eager system with an ownership hierarchy).
In cases like these the value is actually all in memoizing/sharing the equals
cutofff comparison, since the get
is too fast to be worth memoizing but (depending on the contents and the custom equals) equality might not be so fast.
Stuff like this selectAt
-- and, honestly, most uses of the watched
/unwatched
callbacks -- are more often used for exposing reactive models than used directly by views. So maybe your model has some big internal map structure, and you want to let people react to changes in whichever particular entries they're interested in: you don't know which keys they care about, and you don't know what the lifetimes of those interests are. You may want to make multiple reactors share selector computeds under the hood, without leaking computeds all over the place that nobody is interested in anymore... so you reach for something like this, and expose an API where your callers don't have to worry about "releasing" keys when they're done but you still get to share.
@shaylew Ah, thank you!
I'll go ahead and close this since my question here was answered.
I'm struggling to come up with one. It also seems redundant, since you can just do this: