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

Track async setter state #204

Closed
resynth1943 opened this issue Nov 21, 2020 · 8 comments
Closed

Track async setter state #204

resynth1943 opened this issue Nov 21, 2020 · 8 comments

Comments

@resynth1943
Copy link

resynth1943 commented Nov 21, 2020

Hello, and thanks for developing Jotai (it's awesome!) 👋

Although, I have a specific use-case where tracking the async setter state would be incredibly useful.
I use atoms for accessing data in persistent storage (e.g. IndexedDB), which isn't a synchronous API.
Therefore, I'd like to show a loading state between setting the value of an atom with an async setter,
and the value actually being reflected in storage.

Is this possible with Jotai?

@resynth1943
Copy link
Author

resynth1943 commented Nov 21, 2020

Something like this would do nicely:

const [value, setValue, valueWriting] = useAtom(accessToken);

return (
  <SettingsItem value={value} onValueChange={setValue} showLoadingSpinner={valueWriting} />
);

EDIT: the type definition for setValue seems to return Promise<void> in some cases, so this should be possible with a custom hook. Let me write something up, and I'll update the issue accordingly.

@resynth1943
Copy link
Author

export function useAsyncAtom<TValue> (atom: WritableAtom<TValue, TValue>) {
    const [value, setValue] = useAtom(atom);
    const [loading, setLoading] = useState(false);

    const set = useCallback((update: TValue) => {
        setLoading(true);
        const asyncSetterState = setValue(update);

        if (__DEV__) {
            invariant(asyncSetterState == null, [
                'useAsyncAtom: Handling a non-async setter. If your atom doesn\'t have an async setter,',
                'you should probably call useAtom instead.'
            ].join('\n'));
        }

        if (asyncSetterState != null) {
            asyncSetterState.then(() => setLoading(false));
        } else {
            // Immediately set the loading value to false.
            // We shouldn't really be handling sync setters.
            setLoading(false);
        }
    }, [atom, loading]);

    return [value, set, loading];
}

No idea if it works, but here's my first try.
I'm fairly new to React, so I'm learning as I go.

@dai-shi
Copy link
Member

dai-shi commented Nov 22, 2020

EDIT: the type definition for setValue seems to return Promise in some cases, so this should be possible with a custom hook.

Exactly! That's how I expect for this use case.

@dai-shi dai-shi closed this as completed Jan 10, 2021
@dylanwatsonsoftware
Copy link

Hmm the problem with the implementation above is that it would only return loading, if the setter was called from within the same component.

Not really sure how yet, in a generic way, but would be nice to have the loading state shareable throughout the app. The only way I can see to do that though, is to have a separate loading atom for each piece of data stored in an atom.

@dai-shi
Copy link
Member

dai-shi commented Aug 29, 2021

the type definition for setValue seems to return Promise in some cases

This is no longer true. Related issue: #607

The only way I can see to do that though, is to have a separate loading atom for each piece of data stored in an atom.

Yeah, like this, right?

const dataAtom = atom(null)
const inProgressAtom = atom(false)
const asyncWriteAtom = atom(null, async (_get, set, url) => {
  set(inProgressAtom, true)
  try {
    const response = await fetch(url)
    const data = await response.json()
    set(dataAtom, data)
  } finally {
    set(inProgressAtom, false)
  }
})

Not too bad?

One thing I suppose is the community is not yet ready for best practice about how to deal with Suspense and async actions.

That said, jotai's async write will suspend, so you can do this instead.

const dataAtom = atom(null)
const asyncWriteAtom = atom(null, async (_get, set, url) => {
  const response = await fetch(url)
  const data = await response.json()
  set(dataAtom, data)
})

const Component1 = () => {
  useAtom(asyncWriteAtom) // this will suspend
  //...
}

const Component2 = () => {
  useAtom(asyncWriteAtom) // this will also suspend
  //...
}

Does it look a generic way?


FWIW, we have a new issue about async get without Suspense: #672

@dylanwatsonsoftware
Copy link

Yeah seems reasonable and you can always wrap this via a simple method to hide the implementation anyway!

@maikeriva
Copy link

Hello @dai-shi, thanks for creating Jotai! I'm sorry for hijacking this issue, but I'm having troubles to understand your last response.

I am also using an async setter, and I would like to track its execution state (for example, to disable a button while in progress). While I can use Suspense with atoms with async getters, I don't see it working with async setters following your last example.

Here's a trivial example:

const myAtom = atom("hello")
const myAtomSetter = atom(null, async (get,set) => {
    await someAsyncFn()
})

const Content = () => {
    const [,setter] = useAtom(myAtomSetter)
    
    return (
        <button onClick={() => setter()}>Press me</button>
    )
}

const App = () => {
    return (
      <Suspense fallback={<p>Loading...</p>}>
        <Content />
      </Suspense>
    )
}

In this example, the Suspense fallback doesn't seem to trigger when the button is pressed. I can define an isLoading state and set it immediately before calling the setter, but I think there must be a more streamlined way to do it, either with loadable or with the Suspense block, as it is a common use case.

Thanks a lot for your work!

@dai-shi
Copy link
Member

dai-shi commented May 30, 2023

the Suspense fallback doesn't seem to trigger when the button is pressed.

Async write no longer suspends.
This is way too old issue. Please open a new discussion if you need further question.

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

No branches or pull requests

4 participants