Prefetch and cache chained async state?
See original GitHub issueHello, 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
}
Issue Analytics
- State:
- Created 3 years ago
- Comments:8 (5 by maintainers)
Top GitHub Comments
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!
@calumjames exactly! You could even fetch all 3 in parallel and use the
waitForAll()
utility to wait for them (or just usesnapshot.getLoadable()
sequentially for the same effect if you don’t care about waiting for them):Recoil will be smart enough to prevent running a selector unnecessarily more than once, so even if
userQuery
andfatherDetailsQuery
both callmotherDetailsQuery
,motherDetailsQuery
will only run once