dyo / dyo

Dyo is a JavaScript library for building user interfaces.

Home Page:https://dyo.js.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Standardize generator functionality between async and non-async

Zolmeister opened this issue · comments

Currently async iterators and non-async iterators work differently.
I propose that they work the same, where yield is only used for control-flow and not producing elements (see #112).

Thus, if the iterator yields a promise then the component should be treated as async (and the return value as the render element).

This allows async hooks without async components.

async function* useHook() {
  yield await Promise.resolve(null)
  useMemo(_=> 'x')
  return 'xxx'
}

function* Component() {
  a = yield* useHook()
  return h('div', a) // <div>xxx</div>
} 

Currently, to get this to work the component must be async

async function* Component() {
  a = yield* useHook()
  return h('div', a) // <div>xxx</div>
} 

Edit: Note that this only applies to function components that return iterators.

@Zolmeister
May I ask what output the following demo should have according your proposal?
https://codesandbox.io/s/intelligent-moser-c4cjt

@mcjazzyfunky Because yield now only represents control-flow, the output would be:

ignored-1
ABC
ignored-2
ignored-3

@Zolmeister What's the difference new Set(['A', 'B', 'C']) and the others? They both produce sync iterators. Why treat them differently?

Oh, yes. My mistake. It would not have ABC (it would be undefined)

ignored-1
ignored-2
ignored-3

Thanks for the responses.
Actually I think new Set(...) and function* () {... } should really be treated differently because they are indeed quite different. A set is an iterable (I do not know whether that's the correct ES201x term - but I think you know what I mean) as it has a property someSet[Symbol.iterator] while generator functions do not have such a property (only the return value of a generator function, the iterator, has a [Symbol.iterator] property)

So I think there are good reasons that the result of my demo should indeed be:

ignored-1
ABC
ignored-2
ignored-3

Otherwise the whole support for iterables (except for arrays) in Dyo would be killed.
Using collections/iterables other than arrays would require an explicit iterable to array conversion (using Array.from(...) or whatever).

For example something like the following would not be supported out of the box any longer:
https://codesandbox.io/s/purple-sound-v737x
(in this particular case you would have to convert the lazy sequence to an array explicitly before using it as a child node)

@Zolmeister In your example:

function* Component() {
  a = yield* useHook()
  return h('div', a) // <div>xxx</div>
} 

You wouldn't need async generator component to archive this, normal async components work fine.

async function Component() {
  const a = await useHook()
  return h('div', {}, a)
} 

or raw promises

function Component() {
  return h('div', {}, useHook())
} 

Async generators allow you to render multiple states:

async function* Component() {
  yield 'Loading...'
  const a = await useHook()
  yield h('div', {}, a)
} 

Which would first render 'Loading...' immediately, then after the useHook has resolved render the div, async generators are uniquely unlike normal async components or normal sync generators.

@thysultan Unfortunantely due to the mechanics of hooks, normal async components are crippled (ref #111).

e.g.

async function* Component() {
  yield 'Loading...'
  const a = await useHook()
  // TypeError: Cannot read property 'owner' of null
  const b = await useHook()
  yield h('div', {}, a, b)
} 

Therefore you will almost always want to use the generator style when handling inline-promises.

@mcjazzyfunky I can get behind that. Function components that return generators should be treated differently than generator components.

Why are the hooks interlaced like that instead of:

async function* Component() {
  const a = useHook()
  const b = useHook()
  yield 'Loading...'
  yield h('div', {}, await a, await b)
} 

@thysultan If the hook is async, you can only have one (regardless of interlacing). Some of my hooks are async.

async function* Component() {
  const a = await useHook()
  // TypeError: Cannot read property 'owner' of null
  const b = await useHook2(a)
  yield 'Loading...'
  yield h('div', {}, a, b)
} 

Edit: I see, you await at the end. Imaging the value from one hook needs to be passed to another.

@Zolmeister I still don't understand, when you use hooks like that you are going against the call order rule of hooks. It is the equivalent of using hooks in a for-loop?

What exactly are you trying to do with the useHook hook?

@thysultan
This may be quite an unpleasant question but are you really sure that these "async generator function components" and "promises as elements" features are actually good ideas?
Are there really non-trivial "async generator function components" that behave in a way someone would like? For example when the parent component will be updated but the changes are actually not relevant for the inner async generator function component then the promises inside of the async generator function will nonetheless start to be resolved again.

The following three demos for example do not behave like as I would expect or as I would like:
(1) https://codesandbox.io/s/flamboyant-wescoff-69ri3
(2) https://codesandbox.io/s/bold-rubin-x63s8
(3) https://codesandbox.io/s/romantic-snyder-esuvo

This demo shows a behavior as I would expect:
(4) https://codesandbox.io/s/silly-snowflake-tmrhx

@thysultan that's a good point, call-order might break subtly. e.g. getting this to work might be tricky:

X = ->
  useState()
  yield Promise.delay(Math.random() * 100)
  useState()

h 'div', X, X, X

My initial purpose for this feature was to allow hooks which are 'sync' server-side but 'async' client-side. e.g.

useStream = (fn) ->
  return if window?
    useResource(fn)
  else
    useMemo fn, []
X = ->
  user = yield useStream(-> fetch '/user')
  [counter] = useState(0)

However, after some consideration (especially given the comments by @mcjazzyfunky #113 (comment)) I think this is not an efficient approach. I'm closing #114, however I still think this feature is interesting and worth considering.

@mcjazzyfunky I agree some of the demo's present unexpected behaviour.

'sync' server-side but 'async' client-side

@Zolmeister async works just as well server-side.

useResource can be considered a sync hook since it uses suspense/caching + throwing a promise, so it acts like a sync hook, meaning your example wouldn't need async/yield, unless i'm missing something?

X = ->
  user = useStream(-> fetch '/user')
  [counter] = useState(0)

We could relegate async/promise values to act like the useResource hook that works with Suspense.

@thysultan excellent point. I forgot we actually have async hooks via Suspense (a bit leaky, but good enough).

The call-order issue #113 (comment) I think makes this proposal a non-starter. I'm going to close this issue, thanks everyone for the feedback.