-
-
Notifications
You must be signed in to change notification settings - Fork 614
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
loadable
API for using async atoms without suspense
#672
Comments
Thanks for opening this up.
I still believe most people want to avoid ErrorBoundary if they are not ready for Suspense. I generally like symbols, so // with symbol
if (value === LoadingSymbol) {
return <>Loading</>
}
return <>{value.text}</>
// with loadable object
if (value.loading) {
return <>loading</>
}
return <>{value.data.text}</> Or, am I missing something?
I thought it would be easy to implement for writable atoms, because it's just delegating. // with writable atom support
const useLoadableAtom = (anAtom) => useAtom(lodable(anAtom))
// without writable atom support
const useLodableAtom = (anAtom) => [
useAtomValue(lodable(anAtom))
useUpdateAtom(anAtom)
] Not a big deal? I'm fine with only supporting |
Otherwise, we make I wish we get some more feedback from someone. |
I don't think you are, there really isn't that much more typing with a loadable type API. It's just that in TS you'd need to do a check for errors even if you know your promise won't throw.
The way I see it, if an atom throws (which may be due to a bug, not just a fetch failing for example), it'd be weird that the error wouldn't propagate to the component in the same way Loadables make sense when the expected error conditions are expected, like fetching data. Having them return errors in the event of bugs feels weird to me. Furthermore, error boundaries are an officially supported API recommended by React, suspense is still communicated as experimental by the React team.
It doesn't, true! |
@dai-shi if you're still not convinced, I'm happy to go with a pure It'd be pretty trivial to write my own util using loadable:
|
Yes, this is true.
Yeah, I feel like we should go with the pure
This looks nice! And, it's actually convincing that So, the signature will be: function loadable<Value>(
atom: Atom<Value>
): Atom<{ loading: true } | { loading: false; hasError: true; error: unknown } | { loading: false; hasError: false; data: Data }> Hm, we could follow reocil's api: https://recoiljs.org/docs/api-reference/core/useRecoilValueLoadable function loadable<Value>(
atom: Atom<Value>
): Atom<{ state: 'loading' } | { state: 'hasError'; error: unknown } | { state: 'hasData'; data: Data }> what do you think? |
I think following Recoil's API here is a good idea, personally. |
cool. We could make it closer, but it can't be really the same, so |
This makes me so happy to see! I think this will enable a lot more usage from folks who maybe were hesitant due to suspense only support. |
This is extremely exciting to see! I totally second @aulneau's sentiments. This is massively useful even for apps where Suspense is fully embraced, but a few use-cases don't fit any existing design pattern for Suspense. As for readable/writeable - for now all the real use cases I had encountered in my production app only involve readable atoms (e.g suspending something on initial cache load but then refreshing the cache in the background without suspending, feed background refetching from graphql, pull-to-refresh in React Native and a few more similar tasks) - so there's real utility even in a readable only solution. Right now, because of the complexity that a non-suspense atom introduces, I opted out of Jotai for those parts of my code, and I do something similar to this (in this use-case I am caching JWK files for oAuth JWT token validation to allow my app to have the authentication public key tokens be rotated safely without incurring the cost of waiting for an HTTP request every time I launch the app): /**
* This will fetch a new copy of the JWKS and put it into AsyncStorage for the next launch. If keys are rotated
* this will automatically make the auth token fail signature verification.
*/
export const useToken = (): Auth0Token | null => {
const [auth] = useAtom(authAtom);
const [cachedJwks, setCachedJwks] = useAtom(cachedJwksAtom);
const { data } = useQuery("freshJwks", () =>
fetch(jwkUrl).then(async (res) => {
const json = await res.json();
setCachedJwks(json.keys);
return json.keys;
})
);
const jwks: Auth0Jwk[] = data ? data : cachedJwks;
// ... the rest of the code The main drawback of this approach of course is that it makes it harder to derive anything further down the line from this kind of hook. But if there was an easy way to make some atoms opt out of suspense, it would make this kind of use-case possible. |
cool. Seems like I can get some feedback on a subtle design issue. Option A (which I believe we reached an agreement with)function loadable<Value>(
atom: Atom<Value>
): Atom<{ state: 'loading' } | { state: 'hasError'; error: unknown } | { state: 'hasData'; data: Data }> Option B (supporting stale value)function loadable<Value>(
atom: Atom<Value>
): Atom<{ state: 'loading', data?: Data } | { state: 'hasError'; error: unknown } | { state: 'hasData'; data: Data }> At initial fetch, it's |
Would we be open to a third option? function loadable<Value>(
atom: Atom<Value>
): Atom<{ state: 'loading', data?: Data; error?: unknown } | { state: 'hasError'; error: unknown; data?: Data } | { state: 'hasData'; data: Data }> This is most helpful in that it supports the "Stale while revalidate" pattern that is becoming more popular (and very useful). If we went this route, applications would be able to choose if they wanted to show any of the states:
This would be very similar to what react-query does (they keep the |
I'm open for that. (I may change my mind when I see the implementation, though.) |
@dai-shi @aulneau My understanding of Jotai internals is nascent, but I don't think "stale while revalidate" is possible with an implementation as simple as in #269. At least, the answer in #199 here seems to indicate that. We'd need atom getters to support retrieving stale data, which sounds like a pretty big feature. That being said, I'd love for this to be possible. |
@Pinpickle |
@Pinpickle Just checking if you are still around. |
found this by chance but it's so great!! 💝 |
@Pinpickle Hope you get time for it. We have now docs in the same repo. |
@dai-shi I haven't forgotten this, just super busy at the moment. If this isn't done by the time I get some time to do so, I certainly will! |
@Pinpickle Maybe anytime soon??? |
@dai-shi Can't find any docs on loadable! It's available right? It currently feels weird to have "No Suspense" separated from the "Async part", especially given the tricks inside it :D Async section would look like:
What u think? |
It's not yet available.
I think it was written before we had
This is not what I would expect.
btw, I really appreciate your work on these. |
Great, I'm totally okay with this order! When I tried it seemed to work like a charm! Happy be able to help! |
Oops, I got it wrong. The |
Can loadable be used with an async writable atom? |
const baseAsyncWritableAtom = atom(...)
const readOnlyLoadableAtom = loadable(baseAsyncWritableAtom)
export const writableLoadableAtom = atom(
(get) => get(readOnlyLoadableAtom),
(_get, set, arg) => set(baseAsyncWritableAtom, arg)
) |
My use case is I am loading async data and need to update it later on also and use loadable not suspense. Not sure if it is meant to be used that way. The problem here is I am not able to update the data again .For Example - If i update the data using RefreshControl.
|
const fetchGroupAtom= atomWithDefault(async (get) => {
const response = await fetchGroup();
return response;
}); |
This worked. Yet to explore all the utils and use cases. Also explored how ### atomWithDefault works. |
I am using this with an axios fetch function similar to the fetchGroup() above. When I update the writableLoadableAtom and the fetch fails it throws an error instead of setting the atom to hasError. If I catch the error within the fetch function (as in fetchGroup() above), writableLoadableAtom ends up with 'hasData' but data is undefined. Is there a way to make it update in a way that it doesn't throw but sets hasError? |
Not sure if I follow 100%, but in general there would be two alternative approaches:
Hope it helps. |
Ok I found a workaround. Thanks for the prompt response. |
Prior discussion: #269
Recoil has a
useRecoilValueLoadable
for bypassing suspense and returning a "loadable" object. This is useful in some cirumstances when suspense is not desired, and is also much easier than implementing it via the method recommended in the Jotai docs.No-suspense code example
I'm proposing a read-only
loadable
util which returns an atom that converts an async atom into a sync atom, just like Recoil's loadable utils.Depending on context, consumers may want errors to still be thrown or captured into the loadable object. There are two ways of handling this.
1. Separate
suspendable
/loadable
APIsThis has the advantage of calling a single function without parameters whether you want errors to be thrown or not. The
suspendable
type signature is also much simpler, as we don't need to be able to disambiguate between value and error types.2. Param in the
loadable
functionThis has the advantage of there only being one function. However, a flag with a default value means one option will be more convenient than another.
Outstanding questions:
The text was updated successfully, but these errors were encountered: