Hey Recoil team,
What鈥檚 the standard for calling an async Selector on, say, an onClick event?
Would it be to tie the Selector to an Atom, and then change the value of the Atom?
@jcrowson - Could you provide more context? If you're just asking about reading a selector in a callback, vs during the render, you can do that as follows. Motivations to do this might be to avoid an expensive query until the callback, prefetching, etc. See the useRecoilCallback() docs
function MyButton() {
const onClick = useRecoilCallback(({snapshot}) => async () => {
const results = await snapshot.getPromise(expensiveQuery);
alert(results);
});
return (
<div>
<button onClick={onClick}>My Button</button>
</div>
);
}
Is this what you're asking about?
Hi, Is it possible to get a loading state, until the promise is resolved?
@drarmstr, I think @jcrowson is asking how you can rerun an async selector imperatively? An example use case would be when a user clicks refresh button.
I suppose the general idea is that you only need to fetch your data when your selector's dependencies change, but it makes sense that you may want to refetch data from the server in case it has changed.
This is a pattern in Apollo, for example:
const {data, error, loading, refetch} = useQuery(MY_GRAPHQL_QUERY)
return <button onClick={refetch}>Refresh</button>
As far as I know, in Recoil the only way to do this would be to, as you say, update an atom value.
Code example: https://codesandbox.io/s/recoil-refetch-data-bqjp6?file=/src/App.tsx
Correct, selectors represent derived state. The selector get function is logically "idempotent," that is should always provide the same value for a given set of dependency values or selectorFamily parameters. It may be cached or re-executed. Only use selectors for queries where the query results will not change for the lifetime of the app. If you are trying to have local state as a cache of some remote mutable state, then atoms can be used instead. Effects can subscribe atoms to changes in other state, see these examples
@drarmstr Correct. Thank you for providing some context and a real-world situation. The refresh button scenario is exactly what I was talking about.
I had a case where we needed to post data and wanted to use an async selector to do so, but struggled to figure out the best way. We thought about creating a selector on click, but then couldn't figure out how to use a hook to actually subscribe to it. What we ended up doing was having one async selector that gets the value of a query property on an atom, as long as that value isn't null. On click, we set the query property and the selector did its thing.
Basically this:
const requestAtom = atom({
key: "request",
default: {
request: null
},
});
const postSelector = selector({
key: "post",
get: async ({ get }) => {
const {request} = get(requestAtom);
if (request) {
return await postFunction(request);
}
return null;
}
});
@drarmstr, I think @jcrowson is asking how you can rerun an async selector imperatively? An example use case would be when a user clicks refresh button.
I suppose the general idea is that you only need to fetch your data when your selector's dependencies change, but it makes sense that you may want to refetch data from the server in case it has changed.
This is a pattern in Apollo, for example:
const {data, error, loading, refetch} = useQuery(MY_GRAPHQL_QUERY) return <button onClick={refetch}>Refresh</button>As far as I know, in Recoil the only way to do this would be to, as you say, update an atom value.
Code example: https://codesandbox.io/s/recoil-refetch-data-bqjp6?file=/src/App.tsx
Is there any memory leak risks? recoil will cache all the selector values using the depend atom value as key, if i refresh 10 times, there will be 10 cached results. @drarmstr
such as this demo.
https://codesandbox.io/s/recoil-refetch-data-forked-xc43n?file=/src/App.tsx
@weird94 - Correct, Recoil _currently_ caches all selector results, so incrementing some atom dependency for re-queries will represent a memory leak. That will change as we introduce cleanup of atoms/selectors which are no longer used.
To summarize this issue's request for the ability to imperatively force re-evaluation of a selector: Selectors are "idempotent"/"pure" functions of derived state, so can be cached and aren't intended to be re-evaluated.
One approach for this use-case is to use a dependent atom or a selectorFamily parameter with some unique request ID to issue a new requests. This will lead to memory leaks until we introduce memory cleanup, which is currently being explored.
Another approach is explicit execution of requests, and storing the async results in an atom. This can be done today, though requires manually storing the async pending/error states and losing automatically leveraging React Suspense. Feature request to be able to store promises directly in atoms (#237) would resolve this. Using upcoming planned Atom Effects could also help synchronize state.
After providing memory cleanup, we can re-evaluate if an explicit eviction to re-evaluate selectors makes sense if the above approaches are insufficient.
try this:
export const refreshAtom = atomFamily({
key: "refreshAtom",
default: 0,
});
// hook
export default function useRefreshState(key: string) {
const setState = useSetRecoilState(refreshAtom(key));
function refresh() {
setState((state) => {
return state + 1;
});
}
return refresh;
}
// use for refresh
const refreshSite = useRefreshState("site");
Document query refresh pattern in #676
Most helpful comment
@drarmstr, I think @jcrowson is asking how you can rerun an async selector imperatively? An example use case would be when a user clicks refresh button.
I suppose the general idea is that you only need to fetch your data when your selector's dependencies change, but it makes sense that you may want to refetch data from the server in case it has changed.
This is a pattern in Apollo, for example:
As far as I know, in Recoil the only way to do this would be to, as you say, update an atom value.
Code example: https://codesandbox.io/s/recoil-refetch-data-bqjp6?file=/src/App.tsx