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

Use case for Signal.subtle.{un,}watched options for Signal.Computed? #172

Closed
dead-claudia opened this issue Apr 11, 2024 · 8 comments
Closed

Comments

@dead-claudia
Copy link
Contributor

I'm struggling to come up with one. It also seems redundant, since you can just do this:

function computedWithOpts(body, opts) {
    const state = new Signal.State(undefined, opts)
    return new Signal.Computed(() => {
        state.get()
        return body()
    })
}
@littledan
Copy link
Member

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.

@dead-claudia
Copy link
Contributor Author

@littledan Yes, that was the idea.

@shaylew
Copy link
Collaborator

shaylew commented Apr 13, 2024

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.

@dead-claudia
Copy link
Contributor Author

@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)
})

@shaylew
Copy link
Collaborator

shaylew commented Apr 13, 2024

@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.

@dead-claudia
Copy link
Contributor Author

@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.

@shaylew
Copy link
Collaborator

shaylew commented Apr 13, 2024

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.

@dead-claudia
Copy link
Contributor Author

@shaylew Ah, thank you!

I'll go ahead and close this since my question here was answered.

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

No branches or pull requests

3 participants