atomWithObservable unmounts unexepectedly #2225
Replies: 19 comments 5 replies
-
This doesn't cause the behavior either: const [_, time] = await Promise.all([get(timeDepAtom), get(timeTriggerAtom)]); I would not consider this a high priority, but am very curious how this happens. If anyone is interested, please feel free to dig into it. |
Beta Was this translation helpful? Give feedback.
-
yeah, interesting... just to clarify - is this also not a high priority in usecases which depend on frequent ticks? i.e. is mount/unmount/mount extremely cheap and almost like a noop? |
Beta Was this translation helpful? Give feedback.
-
Never mind. I just forgot that |
Beta Was this translation helpful? Give feedback.
-
hmmm, using |
Beta Was this translation helpful? Give feedback.
-
In my mental model, mount/unmount isn't very cheap compared to read/write, so I'm not happy with it. In practice, it's probably cheap enough. |
Beta Was this translation helpful? Give feedback.
-
🤯 |
Beta Was this translation helpful? Give feedback.
-
okay, I've narrowed it down a bit more, it's whenever there's an initial value set. In BehaviorSubject it's nuanced because it comes through the promise firing right away (and will actually fire twice), but this can also happen if you use a regular Subject and just set an initial value: https://codesandbox.io/s/frosty-shannon-2t9677?file=/src/App.tsx |
Beta Was this translation helpful? Give feedback.
-
edit: "recursion" -> "atoms driving themselves" .. in this case it's not exactly recursion, but rather a subscription mechanism that tries to manage itself via onMount callbacks I tried attacking this from various angles... I was able to get this test to pass with a rewrite of I tried a couple other techniques too. Long story short, I'm currently thinking that at least one of these is true:
I'm curious to hear your thoughts, but fwiw without digging into the details of how the store/dependencies/mounting works - after playing with this code some more, I think it fundamentally feels wrong to have an atom drive itself and so I'm leaning towards 1 being the culprit. Although atomWithObservable is super nice, if it's fundamentally unsound (big if!), as for jotai-tanstack-query (and any other libs that depend on this, if any), yeah, this is potentially bad news... but there's a bit of funkiness with that approach as it is (i.e. the whole mechanism of caching between components isn't beneficial at all, jotai already solves that by sharing state between atoms). I think something like Wondering what you think! I may be completely off here... |
Beta Was this translation helpful? Give feedback.
-
There's still something I'm not 100% following, but here's some thoughts. Basically,
Can you clarify what's atoms deriving themselves?
There's such a chance that mount/unmounting logic has bugs. It's pretty tricky especially with dependency atoms, and I'm not 100% confident with current code. |
Beta Was this translation helpful? Give feedback.
-
Thank you for sharing your insight, it's very helpful!
Pretty much exactly what you said in other words :) But, I'd say that even this example is problematic: const actionAtom = atom(null, (get, set, arg: string) => {
console.log(`doing something with ${arg}`);
});
const timerAtom = atom(null, (get, set) => {
setInterval(
() => set(actionAtom, "next arg"),
500
);
}); I don't mean to take that comment out of the context of that discussion where it was only meant to illustrate the point of separating atoms, I just mean for real-world use where you want to clean up the timer, you only want one instance of it running at a time, etc., I think it's a footgun (if you disagree let me know please!) I think generally the right way to use atoms as that the I don't know the deep details, but I do think these issues are highly related to the difficulty of writing FRP libraries. I know it was dealt with extensively in the design of Rust's futures-signals with the separation of I'm loving jotai btw, very happy to settle into a pattern of best practices and avoid the usage patterns that are less recommended, helps a lot to have clarity on what they are :) |
Beta Was this translation helpful? Give feedback.
-
Yeah, I knew that In general, you can put impure code in |
Beta Was this translation helpful? Give feedback.
-
@tmkx Would you be interested in looking into this and/or #2007 (and #2008) ? |
Beta Was this translation helpful? Give feedback.
-
const timeTriggerObservable = new BehaviorSubject(0);
const timeTriggerAtom = atomWithObservable(() => timeTriggerObservable);
timeTriggerAtom.onMount = () => {
console.log("mount", performance.now());
return () => console.log("unmount", performance.now());
};
export const timeDepAtom = atom(async () => "timeDep");
const timeAtom = atom(async (get) => {
//FIXME: not awaiting this fixes the issue
//i.e. this works (or just not using at all)
//const _unusedDep = get(timeDepAtom);
const _unusedDep = await get(timeDepAtom); // 😈 oops
const time = await get(timeTriggerAtom);
return time;
}); The So, should we unmount dependencies after the promise is fulfilled? (currently it's working in a sync manner, so it will clean dependencies when it encounters with the first |
Beta Was this translation helpful? Give feedback.
-
Thanks for the investigation!
Oh, that sounds too tough. A promise may never fulfill. Using Promise.all can be a workaround, right? If we can't solve it, we could document the workaround. |
Beta Was this translation helpful? Give feedback.
-
yes! |
Beta Was this translation helpful? Give feedback.
-
ref: https://fosstodon.org/@daishi/111020604426323401 Okay, I think it's a doc issue then. Would you or anyone like to work on it? |
Beta Was this translation helpful? Give feedback.
-
Think I was able to fix this issue here: #2631 Tested the changes against the codesandbox reproduction, no more infinite unmount/remount 👌 |
Beta Was this translation helpful? Give feedback.
-
@tien Would you be able to look into this issue as well? Hopefully, this refactor could address this too. |
Beta Was this translation helpful? Give feedback.
-
I recently had an idea for a jotai utility that would encourage defining dependencies in sync, even for async atoms. Here is the discussion thread. Maybe this would decrease the likelihood of encountering this edge-case in the first place. |
Beta Was this translation helpful? Give feedback.
-
Summary
atomWithObservable
unmounts unexepectedly when there is some async dependency. This is particularly bad when the observable is set via some loop, and this bug therefore also affectsjotai-tanstack-query
whenfetchInterval
is used.It does not happen if there is no async dependency (See the "FIXME" note in the reproduction on line 18)
Link to reproduction
https://codesandbox.io/s/angry-bassi-r4326h?file=/src/App.tsx
See the console log, it gets spammed with mount/unmount/mount every tick
Additional notes
timeMount.onMount
callback on line 27 never gets calledatomWithObservable
Check List
Please do not ask questions in issues.
Please include a minimal reproduction.
Beta Was this translation helpful? Give feedback.
All reactions