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

reatom async improvement: reactive refetch #530

Closed
Akiyamka opened this issue Apr 2, 2023 · 5 comments
Closed

reatom async improvement: reactive refetch #530

Akiyamka opened this issue Apr 2, 2023 · 5 comments

Comments

@Akiyamka
Copy link
Collaborator

Akiyamka commented Apr 2, 2023

In my application most common case for fetch some data - an filters changed.
Initially we have some set of default filters, and some data that filtered using that set of filters using back-end api.

Whole pipeline can be described by next steps:

  1. Get initial filters (async) and setup in an "filters" atom
  2. On every success change of filters atom - make request getAnData that send those filters in body

implied that in case when filters changed while getAnData in progress - active request will be canceled
Also important that getAnData inherit his loading and error state from filters, because usually only the last atom in pipline chain connected with ui, and it very handy to get info about all pipeline state from this atom

const fetchInitialFilters = reatomAsync(
  (ctx) => {
    return request<Filters>>('/defaults/filters'),
  },
  'fetchInitialFilters'
)

// filters have subatoms "pending" and "error" after piping with async `onInit(fetchInitialFilters)`
const filters = atom<Filters>([]).pipe(
  onInit(fetchInitialFilters)
);

const fetchList = reatomAsync(
  (ctx, filters: Filters) => {
    return request<Array<Element>>('/api/list', { filters }),
  },
  'fetchList'
)


// list have subatoms "pending" and "error" after piping with `filters` that have those subatoms after piping with async fetchInitialFilters

export const list = atom<List>([]).pipe(
  onAtomChange(filters, fetchList),
)
@artalar
Copy link
Owner

artalar commented Apr 15, 2023

Some thoughts

const fetchFilters = reatomAsync(async () => null as any as string[]).pipe(
  withDataAtom([]),
)
onConnect(fetchFilters.dataAtom, fetchFilters)
const fetchSorting = reatomAsync(async () => null as any as string[]).pipe(
  withDataAtom([]),
)
onConnect(fetchSorting.dataAtom, fetchSorting)

// will automatically call `fetchData` when `fetchFilters` or `fetchSorting` will be fulfilled
const fetchData = reatomAsync.from(
  { filters: fetchFilters, sorting: fetchSorting },
  async (ctx, { filters, sorting }) => {},
)

@artalar
Copy link
Owner

artalar commented Apr 15, 2023

cc @BANOnotIT this is one of the variation of ctx.spy inside reatomAsync

@BANOnotIT
Copy link
Collaborator

BANOnotIT commented Apr 15, 2023

const fetchData = reatomAsync.from(
  { filters: fetchFilters, sorting: fetchSorting },
  async (ctx, { filters, sorting }) => {},
)

@artalar If you do this way you'll change semantics of .from

@artalar
Copy link
Owner

artalar commented Aug 18, 2023

I come with interesting updates! I think we could easily implement a new primitive - AsyncAtom, which is an atom with a Promise of the needed data. We need to store a promise in the state to handle all asynchronous flows and ensure proper error handling. This solves a few underlying questions. So, if you try to observe that kind of atom after its creation and when fetching starts, you receive a promise that could fail. It is important to handle this failure, especially for dependent resources.

Don't worry, if you need to read the end result synchronously, you can use the get or spy methods on the dataAtom of an async atom. Of course, connection of an async atom or its dataAtom triggers the fetching process, like onConnect(theAtom, theAtom).

So, here's how it looks like. You should pass async computed to the first argument, but beware, it still needs to be a pure function, which is achieved by wrapping all IO effects in ctx.schedule.

const searchAtom = atom("", "searchAtom");
const idsListAtom = reatomAsyncAtom(async (ctx) => {
  const search = ctx.spy(searchAtom);
  return ctx.schedule(() => fetch(`/api?search=${search}`));
}, "idsListAtom");
const listAtom = reatomAsyncAtom(async (ctx) => {
  const idsList = await ctx.spy(idsListAtom);

  return ctx.schedule(/* ... */);
}, "listAtom").pipe(withAbort());

When searchAtom changes, idsListAtom recomputes immediately, and listAtom also recomputes immediately and stores a new promise. This allows us to handle the entire chain of operations at any given moment with correct dependencies, making it easy to manage concurrent requests and handle abort strategies.

But there are still two problems.

Limitations:

First. As in the regular atom, you couldn't "spy" asynchronously, this will fall an error. In other words, you couldn't call spy after await, only before. So you should change your code style a little bit:

const cAtom = reatomAsyncAtom(null, (ctx) => {
  // WRONG
  const a = await ctx.spy(aAtom);
  const b = await ctx.spy(bAtom); // Reatom error: async spy

  // CORRECT
  const [a, b] = await Promise.all([ctx.spy(aAtom), ctx.spy(bAtom)]);
});

The second problem is a caching. It is possible to handle primitive values from the spy and implement caching on top of it. However, if the value is a promise, we cannot check cache equality because the real value is unknown for a certain period of time. The main question is how to handle the resolved promise value for the cache. It poses a slight challenge for the internal cache and async atom realization, but we will work on it.

@artalar
Copy link
Owner

artalar commented May 21, 2024

Solved with reatomResource!

@artalar artalar closed this as completed May 21, 2024
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