-
-
Notifications
You must be signed in to change notification settings - Fork 696
fix: cleanup pending promises on unmount #3125
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
Open
dmaskasky
wants to merge
1
commit into
pmndrs:main
Choose a base branch
from
dmaskasky:dmaskasky-pending-cleanup
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+58
−3
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| import { describe, expect, it, vi } from 'vitest' | ||
| import { atom, createStore } from 'jotai/vanilla' | ||
|
|
||
| describe('getMountedOrPendingDependents consistent behavior', () => { | ||
| it('sub to asyncAtom -> syncAtom++', async () => { | ||
| const store = createStore() | ||
| const callback = vi.fn() | ||
| const syncAtom = atom(0) | ||
| const asyncAtom = atom((get) => { | ||
| get(syncAtom) | ||
| callback() | ||
| return new Promise((res) => setTimeout(res)) | ||
| }) | ||
|
|
||
| const unsub = store.sub(asyncAtom, () => {}) | ||
| expect(callback).toHaveBeenCalledTimes(1) | ||
| callback.mockClear() | ||
| store.set(syncAtom, (v) => v + 1) | ||
| expect(callback).toHaveBeenCalledTimes(1) | ||
| callback.mockClear() | ||
| unsub() | ||
| expect(callback).toHaveBeenCalledTimes(0) | ||
| store.set(syncAtom, (v) => v + 1) | ||
| expect(callback).toHaveBeenCalledTimes(0) | ||
| }) | ||
|
|
||
| it('sub to asyncAtom -> syncAtomWrapper -> syncAtom++', async () => { | ||
| const store = createStore() | ||
| const callback = vi.fn() | ||
| const syncAtom = atom(0) | ||
| const syncAtomWrapper = atom((get) => get(syncAtom)) | ||
| const asyncAtom = atom((get) => { | ||
| callback() | ||
| get(syncAtomWrapper) | ||
| return new Promise((res) => setTimeout(res)) | ||
| }) | ||
|
|
||
| const unsub = store.sub(asyncAtom, () => {}) | ||
| expect(callback).toHaveBeenCalledTimes(1) | ||
| callback.mockClear() | ||
| store.set(syncAtom, (v) => v + 1) | ||
| expect(callback).toHaveBeenCalledTimes(1) | ||
| callback.mockClear() | ||
| unsub() | ||
| expect(callback).toHaveBeenCalledTimes(0) | ||
| store.set(syncAtom, (v) => v + 1) | ||
| expect(callback).toHaveBeenCalledTimes(0) | ||
| }) | ||
| }) |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See #3124 (reply in thread)
Still it seems to work better now than before so I would not mind sticking with this partial solution. Can start a new discussion to cover more cases
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having second thoughts about it now:
I've tried to think of possible reasons behind that
recomputeInvalidatedAtoms -> getMountedOrPendingDependentscombo: essentially, why we are making an unmounted atom with a pending promise react eagerly to its dependencies' changes.Came up with this
Suspensecase:This particular case will not be affected by the PR. Although this similar one will:
See https://stackblitz.com/edit/vitejs-vite-qkbef5vo?file=src%2FApp.tsx:
With original jotai it outputs:
With
npm i https://pkg.pr.new/jotai@3125:Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, I have a feeling now that even the first case I've mentioned doesn't really work in some cases. Working on a reproduction link now to prove the point
It all feels like just another implication of performing side effects in render: the code becomes dependent on the fact if render is called at a specific time or not. IMO it feels like a react-specific implementation detail. I know the talk that getters are supposed to be pure, though the fact that jotai supports async atoms and treats promises differently contradicts it in a sence, since starting any async operation, memoized or not, is strictly speaking a side-effect. Thus I take it as a jotai's fundamental design choice
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's the link: https://stackblitz.com/edit/vitejs-vite-2rssqjqx?file=src%2FApp.tsx
What I'm trying to prove there is that it's generally impossible to optimally refresh a pending atom when its component is suspended. When the dep changes there is no way to tell:
Now it feels that optimizing for such cases isn't really a good idea and instead it would suffice to recommend subscribing to the atoms explicity outside of Suspense boundary if they really need that Suspense-time eagerness.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Jotai internals does not resolve promises, that is done in useAtomValue. In the future, I think it makes sense to stop unwrapping promises by default and to return the original promise from useAtomValue instead. Then, it would be up to the implementer to wrap with
use.This is one of the ideas for v3 described in #2889
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, thats true. I wasn't stating anything about jotai resolving promises internally, though. What I meant to say:
await/use(promise)it or not. So even after{ use: false }the renders will stay impureI know and appreciate the idea about
{ use: false }. It will help with this situation to some extent, allowing us to additionallyuseAtomValue({ use: false })outside of suspense boudary, where render-mount-unmount order is a bit more straightforward. It still would be prone to some corner cases unless we only do side-effects in useEffects as react wants us to.