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

Investigate the alternatives #48

Open
AlexShukel opened this issue Apr 26, 2023 · 8 comments
Open

Investigate the alternatives #48

AlexShukel opened this issue Apr 26, 2023 · 8 comments

Comments

@AlexShukel
Copy link
Collaborator

AlexShukel commented Apr 26, 2023

This library currently lacks a lot of functionality. So we need to investigate the alternatives and define core functionality points of only-suspense based data fetching library.

Useful links:

  1. https://blog.axlight.com/posts/diving-into-react-suspense-render-as-you-fetch-for-rest-apis/
@AlexShukel
Copy link
Collaborator Author

Parallel queries

Parallel mechanism for fetching queries that use Suspense API is needed. As for now, each useQuery call is executed consequently even if they could run in parallel.

Solution with Promise.all()

One option would be to create a hook useQueries which will fetch queries in parallel.

  1. Combine all promises in Promise.all() call and throw it.
    With this approach there is no way to get specific key of promise that threw.

  2. make a for loop to consequently call .unwrap()

Solution with .unwrap()

@AlexShukel
Copy link
Collaborator Author

AlexShukel commented Apr 27, 2023

Static query

Query that do not depend on the parameters (a resource) must be cached once. Also, there must be a possibility to invalidate cache sometimes (each N seconds or in case of localStorage, on page refresh, etc...)

@AlexShukel
Copy link
Collaborator Author

Single query

Use case when a component has single query. Just throw a promise immediately.

@AlexShukel
Copy link
Collaborator Author

Dynamic query

The type of query that depends on dynamic arguments. For example, query that depends on:

  1. Tab index - discrete number of keys
  2. Search string - infinite number of keys
  3. Selected chat id - discrete number of keys
  4. Scrolling in endless list of items - infinite? number of keys

Maybe it would be a better option to create another hook for fetching infinite lists. But search query does not return consequential fragments of infinite list, but it still has ~ infinite number of possible keys.

@AlexShukel
Copy link
Collaborator Author

Dependent queries

These types of queries depend on other queries' results. This can be done using composite-call or just by using consequential calls to useQuery.

@AlexShukel
Copy link
Collaborator Author

AlexShukel commented Apr 27, 2023

Use of .unwrap() or Proxy

Now hook useQuery throws a promise immediately, which will cause consequential calls to useQuery fetching not in parallel. So, the following solution might be helpful:

It is possible to treat remote data like local data in a component with Proxy functionality. When accessing some of the data field, proxy get handler will throw a promise, so the component will suspend.

However, using .unwrap() is not convenient for "treating remote data like local", because a component should know that to get the data it must call method unwrap on it.

@AlexShukel
Copy link
Collaborator Author

AlexShukel commented Apr 27, 2023

The current approach using keys to cache queries

The current approach is very simple: each query result is cached by stringified arguments of fetcher function. However, with this architecture it is very difficult to invalidate cache only for specific queries that depend on the mutated query. Consider the following scenario:

EXAMPLE 1
We have a chat application, which has the following components:

  1. Sidebar - the list of all available chats with possibility of searching a chat.
  2. Content - endless list of messages with the possibility of searching a message. Also Content has a header above messages which show some information related to the current chat.

User sends new message in chat. So, all queries that depend on this data must be invalidated / mutated. The question is, how can we do that?

If we identify each query only by keys, then it is hard to manage all relations between queries. For example, we have a query for fetching messages (mark it A), which is being mutated. So, we need to invalidate all queries that depend on messages. Here is the list of dependent queries:

  1. B - chat list (because in Sidebar chat list there is information about last sent message)
  2. C - chat list search by message
  3. D - search by message in chat

So, with current approach we need to manually follow the logic of invalidating caches (imperatively calling .clear() on cache store object). In addition, it is hard to clear cache only for specific query, because if we wrap whole Content component in QueryProvider and clear it on message sent, then there will occur useless cache invalidation on the header of Content, which hasn't changed.

Complex application code is growing in complexity very fast. Imagine that when we change the chatId, we need to clear cache only for one query useInitialMessages, because we do not want to clear cache for information in header. So, we iterate over all keys and delete only those cache records that are related to useInitialMessages.

EXAMPLE 2
We have an issue dashboard application. There is a list of all issues assigned to you sorted by some criteria (for example, by priority). When you change the priority of issue, the list must be updated (invalidate + rerender). So, it would be nice to have the possibility to trigger component rerender based on mutated queries.

The idea is that useQuery could listen for all mutations that should trigger the invalidation. And when receiving the mutation update, perform a force rerender.

Also user could create new issue (trigger a mutation) that should cause update on issue list automatically.

If we provide in useQuery parameters only the arguments and fetcher function, then there is no information about relations between queries, so we could not determine when to update this query. So we need to create such API that will provide the possibility to declaratively define the relations between queries.

@AlexShukel
Copy link
Collaborator Author

AlexShukel commented Apr 29, 2023

Model-oriented caching

The idea is that we have defined dependencies between queries in such a way that query A depends on query B when mutation on query B should cause update on query A.

When user triggers a mutation (when data in DB changes) on particular query, then all queries that depend on it must be invalidated (TODO: decide if library should rerender dependent queries or not).

Imagine the following example:
We have MessageList component that shows all messages and we have MessageInput component that can send a new message. So, the MessageInput component mutates remote data in a DB. When user sends a message, we need to make an optimistic update on useMessages query. That means we immediately update the UI (push new message in the array), and only then if service send_message throws and error, we abort this update or throw an error.
The problem is that we need to have a local state of data to immediately update it.

Also the component MessageInput should have an access to the particular useMessages query to mutate it.
What does it mean to mutate a query?

Possible API

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

1 participant