Replies: 3 comments 15 replies
-
@brucou Thanks Bruno for opening this discussion. Here’s a braindump of what I think we need for SuspenseList parity. What we have so farCurrently, we have async components (via async functions and generators) and the ability to implement the async function Fallback({timeout = 1000, children}) {
await new Promise((resolve) => setTimeout(resolve, timeout));
return children;
}
export async function *Suspense({timeout, fallback, children}) {
for await ({timeout, fallback, children} of this) {
yield <Fallback timeout={timeout}>{fallback}</Fallback>;
yield children;
}
} This implementation notably differs from React in that props aren’t cached in any way. If this This also hints at the sole mechanism for “async coordination” in Crank as of today, which is that each element will wait for all descendant async components to fulfill at least once before rendering to the DOM. Once this has happened however, all bets are off, and async components rerender independently of siblings according to the enqueuing mechanism. If you want async child components to rerender together, you must unmount all of them, so that the same async coordination process can begin again. Implementing SuspenseListI don’t want to implement I initially assumed that we would be able to implement const SuspenseControllerKey = Symbol.for("SuspenseControllerKey");
export function *SuspenseList({revealOrder, tail, children}) {
const controller = new SuspenseController(revealOrder, tail);
this.provide(SuspenseControllerKey, controller);
for ({revealOrder, tail, children} of this) {
controller.prepare(revealOrder, tail);
yield children;
}
}
async function Fallback({timeout = 1000, children}) {
await new Promise((resolve) => setTimeout(resolve, timeout));
return children;
}
export async function *Suspense({timeout, fallback, children}) {
const controller = this.consume(SuspenseControllerKey);
this.provide(SuspenseControllerKey, undefined); // hide the controller from children
for await ({timeout, fallback, children} of this) {
// technically we need to handle Suspense components rendered outside SuspenseList but for now we’re not gonna worry.
const i = controller.getIndex();
await controller.fallbackReady(i);
controller.sendFallback(i, yield <Fallback timeout={timeout}>{fallback}</Fallback>);
await controller.childrenReady(i);
controller.sendChildren(i, yield children);
}
} This pseudo-code describes a The problem with this implementation is that it has limited concurrency. Each The missing piece I alluded to earlier is a solution to this problem. Ideally we would have some kind of way to have the export async function *Suspense({timeout, fallback, children}) {
const controller = this.consume(SuspenseControllerKey);
this.provide(SuspenseControllerKey, undefined); // hide the controller from children
for await ({timeout, fallback, children} of this) {
const i = controller && controller.getIndex();
controller && this.schedule(() => controller.fallbackReady(i));
yield <Fallback timeout={timeout}>{fallback}</Fallback>;
controller && this.schedule(() => controller.childrenReady(i));
yield children;
}
} Now, each ImplementationAsync I also am not 100% certain that the Even if we do that, I’m not sure that the above code will call Allowing Ultimately, I think it’s fine for components to only be able to guarantee that their own child DOM nodes are created when |
Beta Was this translation helpful? Give feedback.
-
I am sorry, I had a typo in my question. P, the parent component renders as
is about knowing whether you rerun the generators (thus recreating iterators from scratch) or you iterate the iterator you already have. My understanding is that if C1 or C2 are called with the same parameters the async generator component should not be rerun. In the other case, it should be. |
Beta Was this translation helpful? Give feedback.
-
@brainkim Thanks for taking the time to write all these explications. I am keen on writing some extra docs (assuming I did figure out correctly how Crank works). The truth is the information is probably already there but it would be more a question of proposing a different organization. Not sure yet what form that would take (could be a tutorial, an how to, or could be a concept link). That aside, I am still in the process of formalizing some semantics that would be general enough to comprise any kind of scheduling. More later on that. |
Beta Was this translation helpful? Give feedback.
-
Alright, this is a work in progress and the idea is to log my mental peregrinations here. When it is done, I'll update the status.
Problem description
We want to address the case where some elements are related logically, and that relation impact the order in which they should be displayed to an user. For instance, we have a list of items that are fetched remotely, and we want the items on that list to appear in some consistent order as they appear in the document source:
Source:
Possible display sequence:
t0:
t1:
The following display is not desired at any point of time (item2 cannot be displayed before item1 is):
tX:
This has obvious inspiration in terms of specifications from React's SuspenseList. The React version have more options (like displaying in reverse order) but for now we won't include that in our specifications.
The rationale here is that ordering the display of logically related items improves user experience. For the nitty-gritty details, there.
Modelization
Our problem is one of scheduling or orchestration. On the one hand we have components that can produce content (e.g., HTML) at arbitrary times for a finite number of times. On the other hand, we need to orchestrate the display of content produced by the components, so that an ordering relation is satisfied.
The content produced by a list of n components can be described with HTML(i,j) where i <= n refers to the ith component in the list, and j refers to jth production of a component. We need a function that, given what has already been produced, when something new is produced, then it produces ordered content (i.e. satisfies an ordering relation).
What has already been produced can be described by an array of (i,j) (let's call it PAST). What is produced is another array of (k,l) (let's call that PRESENT). The following property exists always: for each (k,l) in PRESENT, and j in PAST with (k,j) in PRESENT (k,l) > (k,j) when applicable --- in other words, the lth production of component k comes after the jth, or, the present comes after the past. Then a function order must produce from PAST and PRESENT another content list to display. Let's call that list ORDERED. We have order(PAST, PRESENT) = ORDERED.
A very common case is a list of components which only ever produce content twice (initial fallback content, and final content computed from some remotely fetched resource) or once: static content. A common ordering function allows to display static content always and immediately, while displaying non-static component in increasing order. The ordering function can in general be anything, and should be provided by the user of the API. However common ordering functions should also be provided for the API user to reuse.
Proposal
With those semantics defined, we are left with the problem of actually implementing a mechanism to index the components of the list, index the production of a given component, and then display the ordered content that is computed.
To index component of the list, we propose to use a component
SuspenseList
that wraps around all the components to display in an orderly fashion. The index used for a component will be that of that component in the array of children components of the SuspenseList wrapper:Here RemoteFetch1 appearing first, has index 1.
For the communication of the production of a component, we propose either:
yieldP
property into every participating children. The children can then yield their content directly to SuspenseList.Using event
TODO: analysis of the three possibilities? any other?
need to discriminate the components that are not static from those that are
need to emit an event that carries the index of the emitting component with it?
need to decide the name of the event?
inject a property
emit
, orresolve
or whatever that will directly send to the SuspenseList parent.the event is sent on reception of the remote resource (synchronously or not? to think about when the this.refresh should happen). The component with the remote resource yields as usual. The yielded stuff will be picked by this.refresh()
Crank does not allow components that emit multiple times. This would require a push-based iterator, i.e. a stream! So instead of async generator creating iterators that return promises when called, we need observables generators, that create hot streams that emits on their own and trigger refresh on their own. Well I think anyways. Not sure.
I'll use the example here from React:
We recommend to use also a
<Suspense/>
component but no fallbacks. The Suspense component will handle the state of some asynchronous processes (for instance, started, pending, timeout, error, success...), pass that state as a props of its children and refresh every time that state changes. So we would have:Observe as there is no need to nest Suspense components. Suspense is managing a stream, and coordinating with children via props. We need a suspense component PER STREAM (i.e. per resource in the React example).
The Suspense component works that way:
Its parameters:
return a stream that starts some async. process and emits both control state of the process in flight and data. For standard fetches, this is a simple state machine with a few states. cf. Svelte Suspense
}
TODO:
NOTE:
Therefore, the children prop should be treated as a black box, only to be rendered somewhere within a component’s returned or yielded children. Attempting to iterate over or manipulate the passed in children of a component is an anti-pattern, and you should use event dispatch or provisions to coordinate ancestor and descendant components.
Beta Was this translation helpful? Give feedback.
All reactions