Crank: async generators without props

Created on 28 Apr 2020  Â·  8Comments  Â·  Source: bikeshaving/crank

I think the case of async generators without props have strange ergonomics. Consider this hypothetical login form that checks for a user in cookies/localstorage/whatever

import { getUser, login } from './auth'
export default async function* Login() {
  for await (let _ of this) {
    yield <LoadingIndicator />
    let user = await getUser()
    if (user) login(user)
    yield <UserForm />
  }
}

The for await (let _ of this) is necessary (I think) because async generators are continuously yielded until their iterator finishes. Its awkward because there are no props to collect from this, and so an used variable is necessary.

The only alternative I can think of is even more awkward, calling the iterator directly:

while (true) {
    await this[Symbol.asyncIterator]().next()
    yield <LoadingIndicator />
    let user = await getUser()
    if (user) login(user)
    yield <UserForm />
  }

Are there better alternatives?

Most helpful comment

Hmmmmm the example should actually render three times to show the loading indicator, the user, and then nothing, but it looks like async generators have stopped reusing their return value somehow. That’s a bug đŸ˜© But importantly because the component has returned, you can’t update it until it’s forced to unmount and remount in the tree. This is why it’s important to always put your yields in a loop in generator components in general.

Typically, you should use for await with async generator components. You can actually use while (true) with async generator components. For instance, this will work as you expect and clean up correctly:

async function *Component() {
  let i = 0;
  while (true) {
    await new Promise((resolve) => setTimeout(resolve, 1000));
    yield <div>{i++}</div>;
  }
}

But I don’t recommend doing a while true loop within an async generator components in the docs because the consequences of having an async generator unspool infinitely are terrible. It doesn’t crash the process, but creates a zombie process which consumes more and more CPU because of some weird things with the microtask queue and resource starvation.

Honestly, for (const _ of this)/for await (const _ of this) is the way to go if you want maximum safety in your components. Maybe we could bother the eslint people to ignore this sort of unused variable, but in the meantime you can enable a rule to ignore underscore-prefixed variables (which is what TypeScript’s rudimentary linting features do anyways).

All 8 comments

Here's another alternative that I think should work fine:

https://codesandbox.io/s/mystifying-jang-5f575?file=/src/index.js

/** @jsx createElement */
import { createElement } from "@bikeshaving/crank";
import { renderer } from "@bikeshaving/crank/dom";

const LoadingIndicator = () => <h1>loading</h1>;

const getUser = () =>
  new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve({ name: "bob" });
    }, 2000);
  });

async function* App() {
  yield <LoadingIndicator />;
  let user = await getUser();
  yield <h1>{user.name}</h1>;
}

renderer.render(<App />, document.body);

Damn why didn't I even try that? That's perfect. Is that going to cause any issues implicitly returning undefined if crank every iterates on the generator?

Is that going to cause any issues implicitly returning undefined if crank every iterates on the generator?

Not exactly sure what you're asking here. When the generator returns (after yielding <h1>{user.name}</h1>;,it will never update again. I don't think that will cause issues with Crank but I could be wrong.

Hmmmmm the example should actually render three times to show the loading indicator, the user, and then nothing, but it looks like async generators have stopped reusing their return value somehow. That’s a bug đŸ˜© But importantly because the component has returned, you can’t update it until it’s forced to unmount and remount in the tree. This is why it’s important to always put your yields in a loop in generator components in general.

Typically, you should use for await with async generator components. You can actually use while (true) with async generator components. For instance, this will work as you expect and clean up correctly:

async function *Component() {
  let i = 0;
  while (true) {
    await new Promise((resolve) => setTimeout(resolve, 1000));
    yield <div>{i++}</div>;
  }
}

But I don’t recommend doing a while true loop within an async generator components in the docs because the consequences of having an async generator unspool infinitely are terrible. It doesn’t crash the process, but creates a zombie process which consumes more and more CPU because of some weird things with the microtask queue and resource starvation.

Honestly, for (const _ of this)/for await (const _ of this) is the way to go if you want maximum safety in your components. Maybe we could bother the eslint people to ignore this sort of unused variable, but in the meantime you can enable a rule to ignore underscore-prefixed variables (which is what TypeScript’s rudimentary linting features do anyways).

Oh the sandbox by @ryhinchey was using a beta release. Was losing my mind for a second. https://codesandbox.io/s/nifty-shadow-w8bw6?file=/src/index.js:0-473 This sandbox shows the correct behavior which is to render the return value (nothing). You typically don’t want to return from a generator component unless it’s a final state like something you’d show asking the user to refresh to report a bug.

Thanks @brainkim, I figured that would be the case. I did try the while(true) and saw that the render function ran endlessly. Using return for a final value doesn't seem that bad though, that is actually what I want to happen in this case. I could just return the <UserForm /> for this specific case.

Just know that your component gets stuck on the return value and becomes unable to respond to props: I write about the behavior here: https://crank.js.org/guides/lifecycles under “Returning from a generator.” Happy experimenting!

Yea for login that’s ok. The end result is it gets unmounted.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

malcolmstill picture malcolmstill  Â·  4Comments

xkxx picture xkxx  Â·  7Comments

lukejagodzinski picture lukejagodzinski  Â·  4Comments

brainkim picture brainkim  Â·  4Comments

dfabulich picture dfabulich  Â·  4Comments