-
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
Bug with ScopeProvider
falsely capturing an immer atom
#17
Comments
Just saw the issue :), I can take a look when I have the time, but I cannot guarantee when I will do it. Ping me in 2 weeks if I forget it. |
Unfortunately, I can't put time into trying to reproduce or fix right now. I just stopped scoping the atom for now. If I have time free up to investigate/attempt a fix, I will post here. |
@dai-shi Check this out: codesandbox I've added two lines: // App.js
const immerAtom = atomWithImmer({ counter: 100 });
immerAtom.debugLabel = "immerAtom";
immerAtom.temp = 1; // patched-jotai-scope.js, in the patched store, when the new atom is created
if (scopedAtom.debugLabel === "immerAtom") {
scopedAtom.temp = 1000;
} Open the log console, click "increment" right next to "Counter Immer", and you'll get the point. |
Hmmm, I have some clues. Take jotai-immer for example (I think reducer is similar) export function atomWithImmer<Value>(
initialValue: Value
): WritableAtom<Value, [Value | ((draft: Draft<Value>) => void)], void> {
const anAtom: any = atom(
initialValue,
(get, set, fn: Value | ((draft: Draft<Value>) => void)) =>
set(
anAtom,
produce(
get(anAtom),
typeof fn === 'function'
? (fn as (draft: Draft<Value>) => void)
: () => fn
)
)
)
return anAtom
} Here, the first argument of the jotai-scope/src/ScopeProvider.tsx Lines 53 to 61 in ab16ee3
We expect the jotai-scope/src/ScopeProvider.tsx Lines 32 to 41 in ab16ee3
atomStateMap , the scopedAtom's value is incremented, while the original atom's value is unchanged at all. When we remove the ScopeProvider, the original atom is exposed again, so it rewinds back to the untouched original atom's (initial) value.
|
ah, nice catch... that explains the #16 issue too. |
Maybe refactor |
Yeah, it's confusing to me too. I'll draft a PR later. |
After thinking about this, this is the issue of atomWithImmer and atomWithReducer implementation. Let me draft PRs there. |
Well, now, I don't know how to fix it... |
I'll leave my comment here. I am subject to pmndrs/jotai#2351 fix, because that is quite a common technique. I use this trick/recipe a lot, too. It is hard to tell people why we need to do so, and jotai-scope is relative not so broadly used. Let me think about the implementation. |
@dai-shi I find an intuitive solution (for this specific case), haven't have it tested with other scenarios and fully consider its validity yet. See if that can give you some inspirations. I just add a recursive call of getAtom for the return value. The intuition (my guess) is, the fail case only happens when target is the same atom with this (but it is not scoped because its implementation bypass the ScopeProvider's tracking mechanism), so we explicitly try to track it by calling Not sure if the recursion can always correctly terminate. Besides, I quickly drafted a doc for /**
* When an scoped atom call get(anotherAtom) or set(anotherAtom, value), we ensure `anotherAtom` be
* scoped by calling this function.
* @param thisArg The scoped atom.
* @param orig The unscoped original atom of this scoped atom.
* @param target The `anotherAtom` that this atom is accessing.
* @returns The scoped target if needed.
*
* Check the example below, when calling useAtomValue, jotai-scope first finds the anonymous
* scoped atom of `anAtom` (we call it `anAtomScoped`). Then, `anAtomScoped.read(dependencyAtom)`
* becomes `getAtom(anAtomScoped, anAtom, dependencyAtom)`
* @example
* const anAtom = atom(get => get(dependencyAtom))
* const Component = () => {
* useAtomValue(anAtom);
* }
* const App = () => {
* return (
* <ScopeProvider atoms={[anAtom]}>
* <Component />
* </ScopeProvider>
* );
* }
*/
const getAtom = (thisArg, orig, target) => {
if (thisArg.debugLabel === "immerAtom") {
console.log(
`GETATOM thisArg.temp=${thisArg.temp} target.temp=${target.temp} delegate=${delegate}`
);
}
if (target === thisArg) {
return delegate ? getParentScopedAtom(orig) : target;
}
return getAtom(thisArg, orig, getScopedAtom(target));
}; |
Hmmm, another try: Let me first show my understanding of those two conditions: Those two conditions form four combinations: Therefore, the correct function should be: const getAtom = (thisArg, orig, target) => {
if (delegate) {
return getParentScopedAtom(orig);
}
if (target === thisArg) {
return target;
}
return getScopedAtom(target);
}; This analysis sounds more reasonable, it does solve this case, too. I think it won't break existing tests because we only change the behavior of the second case. Still, I'm not sure if it is thorough. |
I also suggest we change those internal parameter names (delegate, thisArg, orig, target), use clearer terms. |
OK, that doesn't fix #16, needs more investigation. |
Totally agree. If you understand how it's working, can you suggest the terms? |
Fixed in 0.5.0 |
When you define an immer atom and the create a
ScopeProvider
that should not capture the immer atom, it still ends up capturing the immer atom inside theScopeProvider
.Here is an example where we have 3 atoms:
We start out with the mounted
ScopeProvider
. Try incrementing all 3 atoms and then press the button to unmount theScopeProvider
. You would expect to see results:but instead you see:
For some reason, the immer atom ends up scoped within the
ScopeProvider
.Here is the code example:
The text was updated successfully, but these errors were encountered: