merisbahti / klyva

A state management library that follows the React component model
MIT License
55 stars 4 forks source link

Does this allow async computed atoms like jotai does? #2

Open pyrossh opened 3 years ago

pyrossh commented 3 years ago

I would like to try doing something like this,

const sumAtom = atom(async get => {
  get(atomOne)
  return await fetch("/api");
})
merisbahti commented 3 years ago

Hi Peter,

Short answer: nope, not yet.

Long answer:

It's interesting to think about how we would go about doing this.

In jotai's case, what they do is for derived atoms, they use suspense, so the suspense boundary renders the placeholder (loading, spinner, etc) while the atom has no value.

Klyva exists outside of react, so it has no concept of suspense.

Also, in Klyva, so far, all atoms always have a value, for async functions, this would not be the case. So how would we model this in klyva?

One suggestion would be, that if the atom is async, the atom returns a union value, where the value can be in different states: loading | resolved | error

I can make a PR for this.

What we could then do, in the future, is that if useAtom is used on a async atom, it uses suspense so the user does not have to worry about the different states.

pyrossh commented 3 years ago

Hi Meris,

Thanks for the quick reply. The union value makes sense. I was trying to figure out a way to do that (was thinking of finding if it was a function and throwing the promise). The idea that the atom can be updated outside the react ecosystem is actually a very common use case in UI. I tried to use derived atoms and ran into this typescript error,

const atomOne = atom(10)
const atomTwo = atom(20)
const sumAtom = atom((get) => get(atomOne) + get(atomTwo))

function App() {
  const d = useAtom(atomOne)
  const g = useAtom(sumAtom)
  return (
    <div className="App">
      {d}
      {'-'}
      {g}
    </div>
  );
}

Error:

  > 12 |   const g = useAtom(sumAtom)
Argument of type 'ReadOnlyAtom<number>' is not assignable to parameter of type 'Atom<number>'.
  Property 'update' is missing in type 'ReadOnlyAtom<number>' but required in type 'Atom<number>'.  TS2345
merisbahti commented 3 years ago
const atomOne = atom(10)
const atomTwo = atom(20)
const sumAtom = atom((get) => get(atomOne) + get(atomTwo))

function App() {
  const d = useAtom(atomOne)
  const g = useAtom(sumAtom)
  return (
    <div className="App">
      {d}
      {'-'}
      {g}
    </div>
  );
}

Hi, first of all, thanks for the report, it has been fixed here: https://github.com/merisbahti/klyva/commit/2ccdc97cfdacbd9a4bfc34eee0a66f0d42d95059 And will be working in 0.1.6 (see here: https://www.npmjs.com/package/klyva).

The idea that the atom can be updated outside the react ecosystem is actually a very common use case in UI.

Yes, it feels like such a normal case, and this optimizes for that.

pyrossh commented 3 years ago

Thanks it works now. I had built a simple use-promise library to work with react suspense. https://github.com/pyros2097/use-promise/blob/master/index.js using the same concept i just ported useAtom to handle suspense for now to test out whether it will work and it does. The typing doesn't work though.

import React from 'react'
import { ReadOnlyAtom } from './types'
const promiseCache = new Map<any, any>()

export const useAtom = <T>(atom: ReadOnlyAtom<T>) => {
  const [cache, setCache] = React.useState(atom.getValue())
  React.useEffect(() => {
    const unsub = atom.subscribe(value => {
      promiseCache.delete(cache);
      setCache(value)
    })
    return unsub
  }, [atom, cache])
  if (cache instanceof Promise) {
    const v = promiseCache.get(cache)
    if (v) {
      if (v instanceof Error) {
        throw v
      } else {
        return promiseCache.get(cache)
      }
    }
    cache
      .then((res) => {
        promiseCache.set(cache, res)
      })
      .catch((err) => {
        promiseCache.set(cache, err)
      });
    throw cache
  }
  return cache
}
import React, { Suspense } from 'react';
import ReactDOM from 'react-dom';
import { atom } from 'klyva'
import { useAtom } from './utils';

const atomOne = atom(1)
const atomTwo = atom(10)
const sumAtom = atom((get) => get(atomOne) + get(atomTwo))

type Todo = {
  complete: Boolean
  title: String
}

const todoAtom = atom<Promise<Todo>>(async (get) => {
  const one = get(atomOne)
  const res = await fetch("https://jsonplaceholder.typicode.com/todos/" + one);
  return await res.json()
})

function App() {
  const a = useAtom(atomOne)
  const sum = useAtom(sumAtom)
  const todo = useAtom(todoAtom)
  return (

    <div className="App" onClick={() => atomOne.update((v) => v + 1)}>
      {a}
      {'-'}
      {sum}
      {'-'}
      {todo.title}
    </div>
  );
}

ReactDOM.render(
  <React.StrictMode>
    <Suspense fallback={<div>loading...</div>}>
      <App />
    </Suspense>
  </React.StrictMode>,
  document.getElementById('root')
);