-
Notifications
You must be signed in to change notification settings - Fork 4
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
Computed atoms get needlessly triggered again #25
Comments
Thank you for submitting the issue, I know what happens but I am not sure what is the correct recipe. Let me first explain some design considerations, they are mainly about derived atoms:
OK, here comes the issue: In a ScopeProvider's nested children component, when calling So, to make it work, we PESSIMISTICALLY creates a copy of all the atoms called within a ScopeProvider (including atoms depended by them), then calls their read function to detect their dependencies. If they are actually not scoped, what we do is like, making the copy subscribe the original atom, so they look like the same. However, internally, all the atoms within a ScopeProvider are copied. They are different instances. Comming back to your original case, I have two ideas. The first one would be add another API to explicitly tells ScopeProvider that I don't want the atom to be scoped, but I am not in favor of it because that would lead to additional stuff to learn and I am not sure if that's a good implementation. The second one would be decoupling the cache with your atom. You can implement the "cache fetch" code outside of read function and determines if the cache is available in your read function. Would you like to share a piece of code of your async cache? I'm interested in if it is only initialized once and never changes then, or it could be invalid and retrieved at some time. Anyway, I think the recipe is not so difficult. I am not so clear if some deeper assumptions are behind. Async functions are not pure. "read functions only trigger when dependencies change" is true, but unfortunately a scoped atom is not the original atom, so multiple initializations occur for each ScopeProvider. |
I see there are some implications that are more complex than what I thought. For example, what if a derived atom is not scoped, but one of its dependent atoms is, it leads to confusing situations indeed.
Sorry I might have mislead you with my language. What I meant by "cache" was really just the atom itself and it how it buffers the result of a derived atom. For example, it could look like this: import { Suspense } from "react";
import { atom, useAtomValue } from "jotai";
import { ScopeProvider } from "jotai-scope";
const MyObjAtom = atom(async () => {
// Expecting this to only be called once (unless someone explicitly scopes it)
const response = await fetch('/api/myobj');
return response.json();
});
function MyObjView() {
const myObj = useAtomValue(MyObjAtom);
return <div>{myObj}</div>;
}
export default function App() {
return (
<div className="App">
<Suspense>
<MyObjView />
<ScopeProvider atoms={[]}>
<Suspense>
<MyObjView />
</Suspense>
</ScopeProvider>
</Suspense>
</div>
);
} The problem we have is that we constructed most our API requests using plain async atoms like these, which works well enough for our current stage, but whenever someone uses a One workaround I found is manually cache those atoms, a bit like so: const MyObjCacheAtom = atom();
const MyObjAtom = atom(async (get) => {
if (get(MyObjCacheAtom)) {
return get(MyObjCacheAtom);
}
const response = await fetch('/api/myobj');
return response.json();
}); This actually works well as I was prioritizing simplicity with this approach for as long as we don't need more advanced features (e.g. invalidation), but it seems that this simplicity won't work with |
Yes yes your workaround is exactly correct, I can understand what a more intuitive API would like, but unfortunately I don't know how to do that (maybe that's a wont-fix) :/ The key takeaway is One should not expect the read function being called exactly once, even though the atom is not scoped. I'll consider documenting it instead of patching it if that is a rare case. |
Updated readme, close it for now. |
apologies but i don't think i understand the work around. Are you writing to this cache atom somewhere? const MyObjCacheAtom = atom();
const MyObjAtom = atom(async (get) => {
if (get(MyObjCacheAtom)) {
return get(MyObjCacheAtom);
}
const response = await fetch('/api/myobj');
return response.json();
}); |
@yf-yang @RaffaeleCanale curious if you have more detail on how you're using the cache. For context, we have two separate bugs when using One involves a combination of I'm not sure how to use a caching solution with the observable since I specifically need it to recalculate on updates from the URQL cache. Part of my difficulty here is that this also only manifests in our production nextjs build. I have tried hard to create a repro here: https://codesandbox.io/p/sandbox/urql-plus-suspense-plus-scope-mzkct6?file=%2Fsrc%2FApp.tsx and haven't been able to create a consistent one. I have seen an infinite loop intermittently but not often. Whereas, the prod bugs I mentioned happen reliably every time. Any thoughts here would be very appreciated. |
@scamden Is the sandbox public? Following that link tells me the sandbox wasn't found. In the workaround I suggested, the atom So by copying the response of the API in a separate atom (which I also don't scope), then even if my derived atom gets computed again, it's fine as it will just return the value from the So in the end, my problem was really just about derived atoms getting recomputed again despite not being in the scope list, which caused unnecessary API calls. Full disclosure: In the end it was too cumbersome and error prone to cache all our API derived atoms into simple value holders atoms, so we went an alternative route and stopped using |
Is this still an issue? I just merged a full ground-up rewite of jotai-scope. We will create a new release version shortly. In the new implementation derived atoms still get copied (because they may access scoped atoms), but the way they get copied doesn't call their read or init functions. This also fixes atomWithLazy from jotai/utils. |
This sounds exciting! I'm happy to give it a drive when the new release is pushed. It might take me a few days to find the time to test but I'm happy to report back afterwards 👍 |
Whoops it wasn't! Now it is.
If you stopped, what alternative did you choose? I experimented briefly with bunshi personally |
None. For the atoms we intended to scope, we kept them global and manually reset them when relevant. We wanted to scope them to avoid unnecessarily holding onto global memory longer than needed, so for now we just accepted to keep them global nevertheless. I'm sorry I can't be of more help, but I have hopes that this latest feature could truly help. |
Problem statement
Let's consider this example:
Note that I purposely do not pass
AtomA
in the scope provider, so I would intuitively assume that the inner child would read from the same "scope instance" as the parent one. As the read function normally only triggers when dependencies change, I would except it not to trigger again but just return the same cached value as for the parent component.However that turns out not to be the case here. "Computing AtomA" gets logged twice in the console almost as if the atom was scoped.
Additional context
Perhaps this was implemented with the idea that read functions are pure, so even if we compute them again they'll return the same result. And if they depend on other atoms, the value for those would match the parent scope, so it would again return the same value as the parent scope.
However I find that hinders the assumption that read functions only trigger when dependencies change. For example I have use cases with async read functions that fetch data from an API into a cached atom. So if I were to consume those atoms inside unrelated ScopeProviders, then they'd trigger the API fetch again which is an undesired and unpredictable behavior.
The text was updated successfully, but these errors were encountered: