-
@brainkim in case you monitor here more often: there is an interesting discussion on twitter addressing crank, and dan has some questions how crank handle concurrency. Would be great if you could chime in: |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 2 replies
-
Converting to discussion mostly because I want to use that feature more! Thanks for bringing this to my attention, Bruno.
https://twitter.com/dan_abramov/status/1301678841023418370
https://twitter.com/dan_abramov/status/1301680021246038016 I must admit I didn’t really conceptualize the “concurrency problem” on this level. The way I came to Crank’s async API was to work backwards; if we assume that defining components as async functions is useful, what would be the most useful behavior for the application developer? For instance, one nice behavior about async function components in Crank is that concurrent renderings of the same component enqueue, such that there is only one pending call of an async component in the tree at a time. This works based on the same diffing algorithm which updates and unmounts components, and is useful insofar as it prevents simple async components from slamming backends with an async request every keystroke or whatever. The question of “coordinating” async components is one of how asynchrony communicates up the tree to ancestors and with siblings, and if you’re coming from the perspective of React, you might find Crank’s behavior surprising. Crank will only coordinate the first rendering of each element, meaning that if an element has multiple async children, it will render those siblings in unison for the first rendering only. // Normally the counters would be encapsulated within a generator, but in this example we place them outside to show async function component updates.
let i = 0;
async function Number() {
await new Promise((resolve) => setTimeout(resolve, 2000));
return <div>{i++}</div>;
}
const letters = "abcdefghijklmnopqrstuvwxyz";
let j = 0;
async function Letter() {
await new Promise((resolve) => setTimeout(resolve, 3000));
return <div>{letters[j++ % letters.length]}</div>;
}
function *App() {
const handleClick = () => this.refresh();
while (true) {
yield (
<div>
<button onclick={handleClick}>Refresh</button>
<div>
<Number />
<Letter />
</div>
</div>
);
}
} If you run this code, you’ll see that for the first render, nothing appears until both the Is this a problem? Maybe. What I don’t get with the consistency rhetoric in the referenced tweet is why async components are being held to a higher standard that regular stateful components. Async components are just stateful components which work based on promises, and one possible solution to the “tearing” between the Maybe this is an unsatisfying answer, and on some level I agree. I spent a lot of time pondering how to reproduce the basic Suspense component, given that the async function components as I have defined both don’t show anything until the promises fulfill and do not respond to updates while the current run is pending. The question is, how do we define components which can concurrently render fallbacks? We can’t use sync function components, because they aren’t stateful, and the most useful behavior is for them to pass updates to their children transparently, regardless of whether or not they’re pending. We also can’t use sync generator components, because sync generator components are resumed with the DOM result of their yields, implying that rendering has completed by the time the generator is resumed. And even if sync generator components did not have this requirement, you can’t This left async generator components, so in the spirit of my strategy of “choosing the most useful behavior,” I decided that async generator components are continuously resumed once mounted, and that to suspend async generator components when there aren’t new props updates or refreshes we use a special context async iterator async function Fallback({timeout, children}) {
await new Promise((resolve) => setTimeout(resolve, timeout));
return children;
}
async function *Suspense({timeout = 1000, fallback, children}) {
for await ({timeout = 1000, fallback, children} of this) {
yield <Fallback timeout={timeout}>{fallback}</Fallback>;
yield <Fragment>{children}</Fragment>;
}
} You can even reproduce React’s nested suspense behavior, which, as it turns out, preact compat has trouble with (preactjs/preact#2747). function fetchUser() {
await new Promise((resolve) => setTimeout(resolve, 1000));
return {
name: "Ringo Starr"
};
}
async function fetchPosts() {
await new Promise((resolve) => setTimeout(resolve, 2000));
return [
{
id: 0,
text: "I get by with a little help from my friends"
},
{
id: 1,
text: "I'd like to be under the sea in an octupus's garden"
},
{
id: 2,
text: "You got that sand all over your feet"
}
];
}
async function ProfileDetails() {
// Try to read user info, although it might not have loaded yet
const user = await fetchUser();
return <h1>{user.name}</h1>;
}
async function ProfileTimeline() {
// Try to read posts, although they might not have loaded yet
const posts = await fetchPosts();
return (
<ul>
{posts.map((post) => (
<li crank-key={post.id}>{post.text}</li>
))}
</ul>
);
}
function ProfilePage() {
return (
<Suspense fallback={<h1>Loading profile...</h1>}>
<ProfileDetails />
<Suspense fallback={<h1>Loading posts...</h1>}>
<ProfileTimeline />
</Suspense>
</Suspense>
);
} To synthesize this info, one way you can synchronize the let i = 0;
async function Number() {
await new Promise((resolve) => setTimeout(resolve, 2000));
return <div>{i++}</div>;
}
const letters = "abcdefghijklmnopqrstuvwxyz";
let j = 0;
async function Letter() {
await new Promise((resolve) => setTimeout(resolve, 3000));
return <div>{letters[j++ % letters.length]}</div>;
}
async function *Together({children}) {
for await ({children} of this) {
yield null;
yield children;
}
}
function *App() {
const handleClick = () => this.refresh();
while (true) {
yield (
<div>
<button onclick={handleClick}>Refresh</button>
<Together>
<Number />
<Letter />
</Together>
</div>
);
}
} The async generator component makes sure all its children render at the same time by unmounting and remounting its children so no enqueuing behavior occurs. However, this has the unfortunate side effect that the component is cleared of all DOM nodes whenever it updates. You can use special async function *Together({children}) {
let node;
for await ({children} of this) {
if (node) {
yield <Raw value={node} />;
}
node = await (yield children);
}
} The truth is, there currently is no way with Crank to both render the previous value and break diffing so the components unmount and remount to prevent async siblings from tearing. The use of the Ultimately, to implement something like React’s In any case, I think I’m on the right track, and I can’t stress how much easier it is to just be able to await promises in components, without a caching requirement. I think the coordination stuff is a nice-to-have which can be added later, and in the meantime you get async SSR today. Like literally today, without compromise, and I didn’t have to do anything special on the renderer side of things. Ultimately, I think React’s approach of providing framework provided components like
https://twitter.com/dan_abramov/status/1301289346541260805 Amen. |
Beta Was this translation helpful? Give feedback.
-
@brainkim @ryansolid I somehow was not notified of this answer so only discovering it now... Thanks for a detailed answer Brian. Looking forward to SuspenseList with Crank. I was however intrigued by the async SSR comment though. I still have to read ryan's article on isomorphic rendering: https://indepth.dev/the-journey-to-isomorphic-rendering-performance. Will then compare the approaches. |
Beta Was this translation helpful? Give feedback.
Converting to discussion mostly because I want to use that feature more! Thanks for bringing this to my attention, Bruno.
https://twitter.com/dan_abramov/status/1301678841023418370
https://twitter.com/dan_abramov/status/13016800…