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

dynamically parametrized selectors (dynamic range of computeds) #4241

Open
1 of 2 tasks
ducin opened this issue Feb 11, 2024 · 3 comments
Open
1 of 2 tasks

dynamically parametrized selectors (dynamic range of computeds) #4241

ducin opened this issue Feb 11, 2024 · 3 comments

Comments

@ducin
Copy link
Contributor

ducin commented Feb 11, 2024

Which @ngrx/* package(s) are relevant/related to the feature request?

signals

Information

TL;DR; Due to computed caching only the most recent computation, it could happen, that unnecessarily the calculations could be repeated over and over again. This proposal is about a way to cache parametrized/multiple (dynamically) computed values

Example Issue

Let's take a look at the following example (from docs):

type BooksState = {
  books: Book[];
  isLoading: boolean;
  filter: { query: string; order: 'asc' | 'desc' };
};

We can create a computed depending on the current state of the filters:

  withComputed(({ books, filter }) => ({
    visibleBooks: computed(() => {
      const phrase = filter.toLowerCase()
      return books.filter(book => {
        return book.title.toLowerCase().includes(phrase)
      })
    }), 
  })),
const books = [
  {title: "abc 1"},
  {title: "xyz 1"},
  {title: "abc 2"},
  {title: "xyz 2"},
  ...
]

An (angular) computed value is cached for the current value of all its dependencies (current value of filter, e.g. query=""andbooks = [...], all works as expected. But when any dependency change, e.g. filter.query changes, then the entire **visibleBooks` has to be re-computed**. And that could be a heavy operation. And if we keep on switching between two values of filter.query but we have the same books (e.g. fetched once from the API):

  • filter.query == abc // result -> [ {title: "abc 1"}, {title: "abc 2"}, ...] // this result FIRST TIME
  • filter.query == xyz // result -> [ {title: "xyz 1"}, {title: "xyz 2"}, ...] // this result FIRST TIME
  • filter.query == abc // result -> [ {title: "abc 1"}, {title: "abc 2"}, ...] // this result HAS ALREADY BEEN CALCULATED
  • filter.query == xyz // result -> [ {title: "xyz 1"}, {title: "xyz 2"}, ...] // this result HAS ALREADY BEEN CALCULATED
    ...

then each time the computed gets dirty, has to recompute, even though the books is the same and, essentially, books.filterBy('abc') result never changes.

This proposal addresses more advanced usecases which, IMO, will appear sooner or later.

The idea is to:

  • introduce a parametrized computed. The difference: ordinary computed is reactive, but not-parametrized
  • what we need is a range of computeds. And there's no built-in API for creating a dynamic range of computeds. We could run them in a loop, if we know the values upfront, but in a case where the query comes from the user input, we'd have to add them dynamically, which increases the complexity.
  • create a Map of computeds: keys: array of computed parameters, values: native computeds.
  • the withMemo, withParametrizedComputed` (or whatever the name becomes) could be either a function encapsulating the access to the Map (and calling underlying computeds) or a Proxy (which would essentially do the same.

Describe any alternatives/workarounds you're currently using

the only alternative I can think of is:

  • providing just an ordinary selector (non-parametrized) which is automatically cached (natively on angular level), but then filtering (by parameter) has to take place outside. And is not cached 🙁

I would be willing to submit a PR to fix this issue

  • Yes
  • No

Example API Proposal

This is just an example of the API, I'm not bound to the API in any way. Design decisions:

  • caching all possible dependencies (all results for ALL books and ALL filter values) would be RAM-expensive and rather not very useful. Not all keys make sense to be cached
    • example: when books change (new response from the API) then all cached results have no meaning anyway
  • choose which keys make sense to be cached, e.g. here: filter, ass long as books don't change
  • choose, then the cache gets cleared, e.g. when books change.

API: withParametrizedComputed((state, cacheKeyFn, clearCacheMap) => newComputeds

  • state is the same as in withComputed,
  • result newComputeds is the same as in withComputed,
  • cacheKeyFn is a function which returns a cache key, the cache itself is an ES Map (not {} in case the chosen key is an object). Example: ({ books, filter }) => filter (yes, books is unused, left it there for readability)
  • clearCacheMap: default (implicit) is { books: true, filter: true } which means clear ALWAYS whenever either books or filter changes. It gets explicitly overriden by - in our example - { filter: false } which results in { books: true, filter: false } (reset only on books change).

Example:

  withParametrizedComputed(({ books, filter }, ({ filter }) => filter, { filter: false }) => ({
    visibleBooks: computed(() => {
      const phrase = filter.toLowerCase()
      return books.filter(book => {
        return book.title.toLowerCase().includes(phrase)
      })
    }), 
  })),

The underlying ES Map could look like the following:

  • Map { } - initially empty, visibleBooks computed never used
  • Map { "abc" => [bookA, bookB, ...] } - first result of computed, filter=abc
  • Map { "abc" => [bookA, bookB, ...], "xyz" => [bookX, bookY, ...] } - second result of computed, filter=xyz
  • Map { "abc" => [bookA, bookB, ...], "xyz" => [bookX, bookY, ...] } - filter=abc chosen again
  • Map { "abc" => [bookA, bookB, ...], "xyz" => [bookX, bookY, ...] } - filter=xyz chosen again
  • ...
  • (Map { } - books changed, cache cleared, but immediately new visibleBooks value gets computed)
  • Map { "abc" => [bookAnotherA, bookAnotherB, ...] } - first result over new books, filter=abc

Of course the API is far from perfect, it's just a starting point for the discussion.

@rainerhahnekamp
Copy link
Contributor

@ducin would have to read through it multiple times to fully understand it. Are you sure there is no way you can fix it with an equals function in the computed? That one's quite powerful.

Could you add a snippet of how such a parameterized computed would look like? That would help a lot

@ducin
Copy link
Contributor Author

ducin commented Feb 26, 2024

@rainerhahnekamp @markostanimirovic I have significantly updated the description, and provided an example API to better outline the issue.

@rainerhahnekamp
Copy link
Contributor

Thanks for the clarification Tomasz

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

3 participants