Skip to content
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

fix(utils): Initial data for atomWithObservable #1058

Merged
merged 9 commits into from
Apr 4, 2022

Conversation

11bit
Copy link
Contributor

@11bit 11bit commented Mar 12, 2022

Reimplementation of initial data option for atomWIthObservable inspired by #875 + some refactoring and simplification of atomWithObservable code

@vercel
Copy link

vercel bot commented Mar 12, 2022

This pull request is being automatically deployed with Vercel (learn more).
To see the status of your deployment, click below or on the icon next to each commit.

🔍 Inspect: https://vercel.com/pmndrs/jotai/6rmNcNquLaDujuuH2drTj4BF9KN9
✅ Preview: https://jotai-git-fork-11bit-feature-observable-initial-data-pmndrs.vercel.app

@codesandbox-ci
Copy link

codesandbox-ci bot commented Mar 12, 2022

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit a55f7e3:

Sandbox Source
React Configuration
React Typescript Configuration
React Browserify Configuration
React Snowpack Configuration
Next.js Configuration
Next.js with custom Babel config Configuration
React with custom Babel config Configuration

Copy link
Member

@dai-shi dai-shi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, nice to see you work on this!! Please see comments.

@@ -29,69 +29,52 @@ type ObservableLike<T> = {

type SubjectLike<T> = ObservableLike<T> & Observer<T>

type InitialDataFunction<T> = () => T | undefined

export type AtomWithObservableOptions<TData> = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know it's a bit controversial, but we prefer not exporting types.

Suggested change
export type AtomWithObservableOptions<TData> = {
type AtomWithObservableOptions<TData> = {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure

Comment on lines 37 to 39
export type CreateObservableOptions<TData> = {
initialData?: TData
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need this. Leftover?

Suggested change
export type CreateObservableOptions<TData> = {
initialData?: TData
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, accidentally copied that from the previous pr. Thanks for noticing

initialData?: TData | InitialDataFunction<TData>
}
export type CreateObservableOptions<TData> = {
initialData?: TData
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we name it initialValue?

Suggested change
initialData?: TData
initialValue?: TData

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed everywhere to initialValue

function firstValueFrom<T>(source: ObservableLike<T>): Promise<T> {
return new Promise<T>((resolve, reject) => {
let resolved = false
const subscription = source.subscribe({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although this looks cleaner, I think there's a risk of memory leaks.
Such case is, a) we create an atom, b) before getting the first value, c) atom can be unmounted.
We need to unsubscribe this subscription on onMount cleanup.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dai-shi hmm, I thought that the suspended component is not mounted before the promise resolves. So, we don't actually have an option to unsubscribe in any case or do I miss something?

To illustrate, here is an atom that suspends and onMount is never called https://codesandbox.io/s/modern-glade-rutmog?file=/src/App.js:0-413

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, you are right about it. The risk is actually the case onMount is never called. And, it's not covered with the previous impl either...

In jotai/urql code, we have a hacky solution with setTimeout.

let timer: NodeJS.Timeout | null = setTimeout(() => {
timer = null
subscriptionInRender.unsubscribe()
}, 1000)

Given that this isn't ideal, and this is such a rare case anyway. I think I can merge this PR. Maybe, we leave some comments for the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW I've already changed other things. Should I do anything else here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add a comment like this:

// FIXME this implementation is not fully compatible with concurrent rendering.
// we need to deal with the case `onMount` is not invoked after the atom is initialized.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dai-shi I've added the comment. Please have a look

- Remove leftovers
- Rename initialData => initialValue
Copy link
Member

@dai-shi dai-shi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM
Thanks for your contribution!

@dai-shi
Copy link
Member

dai-shi commented Mar 31, 2022

#1058 (comment)
After reading my comment again, I notice I misunderstood something.
I think I confused with several issues.

Issue 1: If we subscribe in render (= read function body), there might not be a way to unsubscribe, leading to memory leaks.
Issue 2: Even if we unsubscribe in the callback, it may not fire.
Issue 3: Ideally, we should just subscribe once, if it mounts soon.

Issue 1 is important, but we unsubscribe in the callback (both before this PR and this PR), so it's not a big issue if the callback is invoked soon.
But, Issue 2 is still present for both. We would like to fix it but the known workaround is only the setTimeout hack done in jotai/query (internally in RQ) and jotai/urql.
Issue 3 is something introduced in this PR. It's only the performance issue, and if subscribe is lightweight, it's not a big issue.

So, if Issue 3 is something we care, we would like to revert the change. What do you think? @11bit

The FIXME comment should be about Issue 2. Let me try clarifying more.

Copy link
Member

@dai-shi dai-shi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait, there is more important issue.
Issue 4: If the value is changed twice the second value is dropped.
I guess it's the regression with this PR.

So, let's revert it, or come up with a better solution.

@dai-shi
Copy link
Member

dai-shi commented Mar 31, 2022

Issue 4: If the value is changed twice the second value is dropped.

Hmm, it's not solved even with the original code without this PR.
My bad again. I'll wait for comments anyway, hoping we have a better solution.

Copy link
Member

@dai-shi dai-shi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The biggest concern is Issue 4, but it's not a regression of this PR.
Issue 3 is the one introduced in this PR, but only a performance concern.

So, at least, it's mergeable.

@dai-shi dai-shi changed the title Initial data for atomWithObservable fix(utils): Initial data for atomWithObservable Mar 31, 2022
@11bit
Copy link
Contributor Author

11bit commented Mar 31, 2022

So, you mean that current implementation can miss values received between first value and befoe onMount, right?

I think solution might be to subscribe only once and do the following:

  1. First value => resolve promise
  2. Store everything that is receiverd before onMount somewhere
  3. onMount => set the last received value from step 2 and then work as usual

@dai-shi
Copy link
Member

dai-shi commented Mar 31, 2022

So, you mean that current implementation can miss values received between first value and befoe onMount, right?

Yes. The previous implementation before this PR can miss them too, but it throws an error in such case. (So, it's sort of a regression?)

2. Store everything that is receiverd before onMount somewhere

"somewhere" will be a variable in the scope, which is fine.
But, then we will have Issue 1. An atom can be initialized and never be mounted.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants