-
Notifications
You must be signed in to change notification settings - Fork 559
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
Async render generator #29
Comments
What would happen if you rerendered |
@j-f1 On client it would first render |
@streamich Here’s the situation I was thinking about: async function* MyComponent({ onClick, buttonLabel }) {
yield (<div>Loading...</div>);
const users = await fetch('/users');
return <div>
<UserList users={users} />
<button onClick={onClick}>{buttonLabel}</button>
</div>;
}
ReactDOM.render(<MyComponent buttonLabel="doThingOne" onClick={...} />, container)
ReactDOM.render(<MyComponent buttonLabel="doThingTwo" onClick={...} />, container) Would |
ReactDOM.render(<MyComponent buttonLabel="doThingOne" onClick={...} />, container)
ReactDOM.render(<MyComponent buttonLabel="doThingTwo" onClick={...} />, container) Would render loading indicator and fetch users only once. But you could force it do it twice, if you wanted, using the ReactDOM.render(<MyComponent key="1" buttonLabel="doThingOne" onClick={...} />, container)
ReactDOM.render(<MyComponent key="2" buttonLabel="doThingTwo" onClick={...} />, container) However, I see a "stale prop problem" in stateless async components — by the time it renders the button the This is not a problem with stateful components however, and for stateless components it could be solved in two ways. Solution 1 - using async function* MyComponent() {
yield (<div>Loading...</div>);
const users = await fetch('/users');
yield (
<div>
<UserList users={users} />
<button onClick={this.props.onClick}>{this.props.buttonLabel}</button>
</div>
);
} Solution 2 - feeding props to generator on every step: async function* MyComponent(props) {
props = yield (<div>Loading...</div>);
const users = await fetch('/users');
yield (
<div>
<UserList users={users} />
<button onClick={props.onClick}>{props.buttonLabel}</button>
</div>
);
} |
Then how would the component be able to update itself when its props are changed? |
What would happen if the second |
The first thing that comes to mind is that it could yield a closure: async function * MyComponent() {
yield (<div>Loading...</div>);
const users = await fetch('/users');
yield () =>
<div>
<UserList users={users} />
<button onClick={this.props.onClick}>{this.props.buttonLabel}</button>
</div>;
} |
There could be a new keyword added to JSX which creates the closure automatically: async function * MyComponent() {
use (<div>Loading...</div>);
const users = await fetch('/users');
use (
<div>
<UserList users={users} />
<button onClick={this.props.onClick}>{this.props.buttonLabel}</button>
</div>
);
} In this example |
BTW, the idea comes from |
I've created a list of use cases, which also includes use cases mentioned in https://gist.github.com/streamich/68b4470a8365f7b7ac6b4f614cdf45d2 |
@streamich The "Load data in steps" example is confusing when compared with the "Loading" example. Is the next yield meant to replace the previous one(in the case of the Loading example), or are they mean to all render(like the case of the "Load data in steps")? |
@thysultan I've tweaked that example slightly, indeed it is not the most clear one, but what it shows is that you can fetch initial batch of users |
@streamich How would something like this work https://jsbin.com/folotu/edit?js,console.
|
Grow the list: async function * NumberList () {
const numbers = [];
for await (const number of asyncRandomNumbers()) {
numbers.push(number);
yield () => numbers.map((number, i) => <div key={i}>{number}</div>);
}
}
ReactDOM.render(<NumberList />, el); Show only the last number: async function * NumberLatest () {
for await (const number of asyncRandomNumbers()) {
yield () => <div>{number}</div>;
}
}
ReactDOM.render(<NumberLatest />, el); |
@streamich, thanks for tagging me here.
@j-f1, when Coroutine receives new props, and they are new (
@streamich I wasn't following, but what's the purpose of yielding a function (when you can just yield JSX)? I use
You can find a code example here: https://github.com/alexeyraspopov/actor-system/blob/master/examples/react-application/modules/MessageMonitor.react.js#L7-L12
Since each import React from 'react';
import Coroutine from 'react-coroutine';
import SomeComplexViz from './SomeComplexViz.react';
export default Coroutine.create(SomeComplexVizContainer);
async function* SomeComplexVizContainer({ someId }) {
const someData = await DataAPI.retrieve(someId);
yield <SomeComplexViz first={someData} />
const anotherData = await AnotherAPI.retrieve();
return <SomeComplexViz first={someData} second={anotherData} />;
} Note: receiving component should handle missing data on its own. |
@alexeyraspopov The purpose of yielding a function was so that the whole async generator does not have to be re-played again when props change, in this example: async function * MyComponent() {
yield (<div>Loading...</div>);
const users = await fetch('/users');
yield () =>
<div>
<UserList users={users} />
<button onClick={this.props.onClick}>{this.props.buttonLabel}</button>
</div>;
} In the above example the idea was that if the async component already rendered |
Interesting... It was intentionally done in react-coroutine, but with checking new props with |
You can still force the iterator to "re-play" using the ReactDOM.redner(<MyComponent key="1" />, el);
ReactDOM.redner(<MyComponent key="2" />, el); Or maybe it could be a rule that if the last yielded value is not a function the iterator will be re-played, otherwise the yielded function will be called on re-renders. |
@streamich I implemented a POC https://jsbin.com/hezaduq/edit?html,output in DIO based on this, it's an interesting pattern. |
@thysultan I will take a look at it tomorrow. On another note, one thing I have mixed feelings about is that these async generator render functions are basically "state containers". I am wondering if the new proposed React.createState = (initialState) => {
return async function * State () {
// ...
for await (const state of stateIterator)
yield () => this.props.children(state, setState);
};
}; |
@thysultan that was fast! What do you think about not needing the stateful components at all because your example could be rewritten as: class Test extends Component {
async * render () {
let index = 0;
while (index++ < 20) {
await new Promise(resolve => setTimeout(resolve, 500));
yield () => <div>index</div>;
}
}
} Basically the generator function "contains" the state inside itself, in the Any problems you ran into while implementing this? |
Yes, i think you can get radically creative with things like animations/user flows if you wanted. async function * SignUp () {
var step = 0
var name = ''
while (true) {
if (step)
yield <h1>Hello {name}</h1>
else
yield <form onSubmit={(e) => e.preventDefault(step++)}>
<input placeholder="Name?" onInput={({target}) => name = target.value}>
</form>
}
}
It was fairly simple mainly because there where prior abstractions that made it that way but i figure it is possible in future async™ React as well. @alexeyraspopov With your work on "react-coroutine" how are you testing async generator functions in NodeJS? |
@thysultan I see he is using |
While async generators are not fully supported by Node.js, I transform them using Babel.
Probably I can now update the presets, it's been a while. |
I don't think IMO, making render() What do you think? |
The new "Suspense" API is built on the predicate of awaiting inside render(via thrown Promises). So i'm sure React could also handle this if it wanted to(subject to implementation limitations i may not know about). |
You can already implement it in user space, also |
That's not "awaiting inside render()". async render() {
const { a, b } = this.props;
const { c, d } = this.state;
await promise;
// <- Continues here after resolve
// a, b, c, d will have old values if they are changed before promise resolves
}
render() {
const { a, b } = this.props;
const { c, d } = this.state;
throw promise;
// <- Can't continue even after resolve
// render() will be called again and will have fresh props and state if they are changed
} render() is completely sync and making it async is a whole different thing than throwing promises. |
When you If What are the inputs? FC: Functional Component
In FCs, Props can be checked by React, Outer Constants do not have to be checked, Outer Variables break your code. Let's assume we have a "Don't use Outer Variables and The problem with class extends Component {
static async render(props, state) {
const inputA = props.input;
const outputA = workA(input);
const inputB = await getInputB();
// We are here and `props.input` have changed which makes outputA invalid.
// How does React stop render() from continuing?
// It can't.
// You would be doing useless work which blocks the main thread.
const outputB = workB(outputA, inputB);
return outputB;
}
} But when you class extends Component {
static *render(props) {
const inputA = props.input;
const outputA = workA(input);
const inputB = yield getInputB();
// `props.input` have changed which makes outputA invalid.
// React throws away the old Generator and calls render() again.
// We never arrive here.
const outputB = workB(outputA, inputB);
return outputB;
}
} (If I'm not terribly mistaken) current suspense mechanism always discards old work, which could be still valid. Generators could help in performance and would be better than suspense when used correctly: class extends Component {
static *render(props) {
const inputsA = props.inputsA;
const outputsA = [];
for (const inputA of inputsA) {
const output = syncWorkA();
outputsA.push(output);
// syncWorkA() was expensive
// Yield so React can work on other stuff
yield;
}
// getInputB() is async
// Yield promise so React awaits and continues with result
const inputB = yield getInputB();
const outputB = workB(outputA, inputB);
return outputB;
}
} Spend some more time on this and we could turn it into two RFCs: "Static render() for Class Components" and "Generator render()". I saw static render() already being discussed, so maybe we can just write the generator RFC assuming static render() is already available. If we go back to On the other hand, implementation of suspense implies that React team needs a way to make render() async. Suspense doesn't suspend and actually stops. With generators you can truly suspend and that is an improvement over suspense. |
The assumption is that it can, and would prioritize fresh work, i.e given the following async function * Foo ({children, time}) {
yield wait(h('h1', 'Loading'), time)
yield h('h1', children)
}
render(h(Foo, {children: 'Hello', time: 200}), container)
render(h(Foo, {children: 'World', time: 100}), container) The last value ("Loading" -> "World") pair would render instead of the last resolved value("Hello"). This escapes the problem of stale data. That is – new(resolved) work can invalidate old stale work that is pending – which is how this is handled in the mentioned PR |
@AlicanC Why are you doing |
Because that is the only place you can do work in a Functional Component. If we can do async or expensive sync work in render() than there will be less cases where we must use Class Components. I have basically tried to give Suspense a generator frontend. If we want both function* MyComponent(props) {
yield <div/>; // Renders intermediary element
const data = yield promise; // Emulates "await promise"
return <OtherComponent data={data} />;
} |
@AlicanC, It's quite difficult to separate yielding JSX and "data" (either data or promises). It also brings more confusion for those who read the code: "are we awaiting or there will be intermediate content?" React Coroutine supports both generators and async generators. For generators it expects yielding "possible promise" (like was presented by Dan) and the returned result is rendered. For async generators, you can yield intermediate content and then await for some data (either by |
ظت |
looks like stateful component can be implemented as a generator so there is no need for the class syntax I think. |
There are some examples of making stateful component using async generator in react-coroutines examples folder |
Even with hooks I'd still like to see something like this/something like https://github.com/Astrocoders/epitath to flatten certain render calls as it supports if/else logic which you sometimes need 👍 |
Please file proposals as PRs, not issues. |
This is not a proposal per se, just wanted to see your thoughts on async generator as a render function.
Stateless component:
Stateful component:
What it solves?
On server side it could render only once the iterator has been fully resolved, while on client it would render all intermediate steps of the iterator.
For example, on client it would render:
<div>Loading...</div>
<UserList users={users} />
On server it would wait until
render
method fully resolves and render the final result only:<UserList users={users} />
The text was updated successfully, but these errors were encountered: