Recoil: Prefetch and cache chained async state?

Created on 23 Jun 2020  Â·  7Comments  Â·  Source: facebookexperimental/Recoil

Hello, I'm using useRecoilValueLoadable with async selectors, and have a chain of selectors that look something like this:

atom --> async selector (user) --> async selector (mother)
                                          \
                                           --> async selector (father)

I would like to prefetch / cache all three async selectors in my main page using useRecoilCallback(). I am having a hard time figuring how to pre-fetch / cache all three fetch calls. The "user" selector is never cached until I load all of the leaf views ("father" and "mother"). Depending on which leaf view I render first, the other leaf selector will make an unnecessary fetch call when rendered. The first async selector in the middle is almost never cached until I render both views. Below is an example of the code. I tried using a combination of useEffect(), useRecoilCallback({snapshot, set}) with intermediate synchronous atoms for state, but no luck at all. The below code resembles the furthest I have gotten so far. The only thing I can think of is to add my own caching mechanism in the "fetch" call, which seems to defeat the purpose of selectors. I would appreciate any help!

export const userIdState = atom({
  key: "userIdState",
  default: ""
});

export const userQuery = selector({
  key: "userQuery",
  get: async ({ get }) => {
    const { data } = await fetchUser(get(userIdState));
    return data;
  }
});

export const fatherDetailsQuery = selector({
  key: "fatherDetailsQuery",
  get: async ({ get }) => {
    const { fatherId } = await get(userQuery);
    const { data } = await fetchFather(fatherId);
    return data;
  }
});

export const motherDetailsQuery = selector({
  key: "motherDetailsQuery",
  get: async ({ get }) => {
    const { motherId } = await get(userQuery);
    const { data } = await fetchMother(motherId);
    return { data };
  }
});

function MyPage = () => {
  useRecoilCallback(({ snapshot }) => async () => {
    // this is the only way I can seem to get some caching of mother/father loadables
    await snapshot.getPromise(userQuery); 
    snapshot.getLoadable(fatherDetailsQuery);
    snapshot.getLoadable(motherDetailsQuery);
  })();
  return (
    <div>
       // render user stuff
       // assume showMother and showFather are false and can be toggled true/false
       {showMother && <Mother />}
       {showFather && <Father />}
    </div>
  );
}

function Father = () => {
  const fatherLoadable = useRecoilValueLoadable(fatherDetailsQuery);
  // render father stuff
}

function Mother = () => {
   const motherLoadable = useRecoilValueLoadable(motherDetailsQuery);
   // render mother stuff
}
bug

Most helpful comment

hey @dliu120 the bad news is that caching of async selectors (specifically, caching of async selectors that are part of async chains like the one you've described above) is currently buggy and not working as expected.

The good news is that we are aware of this issue and have been working on a solution for some time now. We ended up rewriting the implementation of async selectors entirely to account for cases like this.

The gist of the rewrite is that every time a selector's value is requested, we store the chunk of state that the selector was called with, so that if the selector is requested again, we can check to see if the selector is already running against that state given the dependencies that have been discovered so far. If it has, we don't rerun the selector.

Unfortunately the only foolproof way around this for the moment is as you suggested: implementing caching at the fetch layer.

This fix is very high on our priorities so hopefully will be out soon. I'll be sure to link this issue to the corresponding PR/issue when it is opened.

Thanks for opening this and sorry for the trouble!

All 7 comments

cc @csantos42

hey @dliu120 the bad news is that caching of async selectors (specifically, caching of async selectors that are part of async chains like the one you've described above) is currently buggy and not working as expected.

The good news is that we are aware of this issue and have been working on a solution for some time now. We ended up rewriting the implementation of async selectors entirely to account for cases like this.

The gist of the rewrite is that every time a selector's value is requested, we store the chunk of state that the selector was called with, so that if the selector is requested again, we can check to see if the selector is already running against that state given the dependencies that have been discovered so far. If it has, we don't rerun the selector.

Unfortunately the only foolproof way around this for the moment is as you suggested: implementing caching at the fetch layer.

This fix is very high on our priorities so hopefully will be out soon. I'll be sure to link this issue to the corresponding PR/issue when it is opened.

Thanks for opening this and sorry for the trouble!

The gist of the rewrite is that every time a selector's value is requested, we store the chunk of state that the selector was called with, so that if the selector is requested again, we can check to see if the selector is already running against that state given the dependencies that have been discovered so far. If it has, we don't rerun the selector.

Thanks @csantos42! Does that mean we can write our code like OP has here and we'll just get the performance improvements without having to rewrite anything in a future Recoil update?

@calumjames exactly! You could even fetch all 3 in parallel and use the waitForAll() utility to wait for them (or just use snapshot.getLoadable() sequentially for the same effect if you don't care about waiting for them):

await snapshot.getPromise(
  waitForAll([
    userQuery,
    fatherDetailsQuery,
    motherDetailsQuery,
  ]),
);

Recoil will be smart enough to prevent running a selector unnecessarily more than once, so even if userQuery and fatherDetailsQuery both call motherDetailsQuery, motherDetailsQuery will only run once

@csantos42, heh I discovered waitForAll shortly after my comment here. Thank you for the further detail. This library is brilliant!

hey @dliu120 the bad news is that caching of async selectors (specifically, caching of async selectors that are part of async chains like the one you've described above) is currently buggy and not working as expected.

The good news is that we are aware of this issue and have been working on a solution for some time now. We ended up rewriting the implementation of async selectors entirely to account for cases like this.

The gist of the rewrite is that every time a selector's value is requested, we store the chunk of state that the selector was called with, so that if the selector is requested again, we can check to see if the selector is already running against that state given the dependencies that have been discovered so far. If it has, we don't rerun the selector.

Unfortunately the only foolproof way around this for the moment is as you suggested: implementing caching at the fetch layer.

This fix is very high on our priorities so hopefully will be out soon. I'll be sure to link this issue to the corresponding PR/issue when it is opened.

Thanks for opening this and sorry for the trouble!

How do you detect that the states the selector depends on changed? Do you eventually provide an equals() implementation possibility so that complex states can be compared to each other according to the business rules behind them?

Yeah, probably something like that. That’s being tracked with #314

On Jun 30, 2020, at 04:06, Zied Hamdi notifications@github.com wrote:



hey @dliu120https://github.com/dliu120 the bad news is that caching of async selectors (specifically, caching of async selectors that are part of async chains like the one you've described above) is currently buggy and not working as expected.

The good news is that we are aware of this issue and have been working on a solution for some time now. We ended up rewriting the implementation of async selectors entirely to account for cases like this.

The gist of the rewrite is that every time a selector's value is requested, we store the chunk of state that the selector was called with, so that if the selector is requested again, we can check to see if the selector is already running against that state given the dependencies that have been discovered so far. If it has, we don't rerun the selector.

Unfortunately the only foolproof way around this for the moment is as you suggested: implementing caching at the fetch layer.

This fix is very high on our priorities so hopefully will be out soon. I'll be sure to link this issue to the corresponding PR/issue when it is opened.

Thanks for opening this and sorry for the trouble!

How do you detect that the states the selector depends on changed? Do you eventually provide an equals() implementation possibility so that complex states can be compared to each other according to the business rules behind them?

—
You are receiving this because you commented.
Reply to this email directly, view it on GitHubhttps://github.com/facebookexperimental/Recoil/issues/390#issuecomment-651723799, or unsubscribehttps://github.com/notifications/unsubscribe-auth/ABDVJ2AFYOFC4HI4FE7NWITRZHBI5ANCNFSM4OFMACQQ.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

aappddeevv picture aappddeevv  Â·  3Comments

julienJean99 picture julienJean99  Â·  3Comments

ymolists picture ymolists  Â·  3Comments

robsoncezario picture robsoncezario  Â·  3Comments

adrianbw picture adrianbw  Â·  3Comments