Seems like getting a ref as shown in the documentation doesn't work on the first render

results in

on the first render and

after button click
Yeah this is my bad! I wrote that ref code real quick without thinking about it and what youâre pointing out highlights the difficulty of using sync generators with refs. Your code would work if you converted it to an async generator but sometimes you want things to happen synchronously and I gotta respect that. The problem is that synchronous generators suspend at their yield, which means that if you assign the yielded expression to something, it will actually have paused right before the assignment, which is kinda frustrating. And the ref will only be the value previously rendered which sucks.
The solution is either to use an async generator, which continuously executes while the component is mounted, or do something like this:
function *MyComponent() {
let ref;
Promise.resolve().then(() => this.refresh());
ref = yield <div />
while (true) {
// do nasty imperative stuff to the div
yield <Copy />;
}
}
This way the component refreshes once and has the ref variable set to the yield. This is sorta emulating the componentDidMount workflows for React.
I need to update the docs to get them to work but this is basically how to do it with sync generators. The great thing is that Crank doesnât care if you perform side-effects in the course of rendering so it doesnât really matter if youâre doing stuff before update or after update.
This is a real bummer about the difference in execution pattern as I'm sure it will be an issue many run into. I imagine this should be highlighted in the FAQ.
Believe me I tried really hard to make refs work in sync generators just like they do in async generators, but I couldnât figure it out. I thought about stepping through generators twice per update, to make sure that refs were assigned, but this made the execution of generators more opaque and didnât even solve the problem. I thought maybe we could have a callback API for getting the ref but this seemed unwieldy.
Ultimately, I think the âyield something once and refreshâ-approach solves most use-cases for sync generators with refs, and pretty much maps to what you would do with React anyways.
I will endeavor to make this issue more clear in the docs! If you have any ideas how Iâm all ears.
I'll think on it
This is a question I've been thinking of since encountering the library as well. It highlights a potential issue that components expressed as {sync | async} {function | generator}s have really distinct execution models and differ in their capabilities, in ways that weren't obvious to me at first.
returnyieldfor await (props of this) wrapper is always necessary, so in effect they are suspended at waiting for the next props: this[Symbol.asyncIterator].next()To me it seems that the runtime behavior of async generators is very different from the other 3, and that tripped me up at first and didn't dawn on me until I started experimenting with refs with different function types. I wish if they could behave more consistently somehow.
The vast difference between the 4 models shows up again and manifests in what you can do with each function type, in non-obvious ways. For refs:
for await loop naturally delineates the component lifecycle: pre-render, render (yield), and post-render (what comes after yield). But they are also the most complex.I wish that non-async generator components are able to use functionalities such as refs and post-render hooks without having to be converted to async generators. This has effects on real-life usages: A component that starts off simple as a sync function would need to be converted to an async generator to use post-render hooks, and it's a significant refactor and if it happens too much it slows down dev velocity. Whereas in React, doing so is "just" adding an useEffect and useRef without needing to refactor the whole component.
I thought maybe we could have a callback API for getting the ref but this seemed unwieldy.
Though it may seem like âgiving upâ to go back to old patterns, would simply re-using Reactâs approach of:
const myRef = React.createRef();
<div ref={myRef}></div>
... solve things at all?
Iâm not accustomed to generators, but I feel like passing in an object (myRef) to be populated upon the moment of the renderer performing props-setting would allow the ref to be populated before suspension of the function.
Remember that many library consumers will want refs to not just the most basal component in the tree, but nested ones too, and I can only see something like this enabling that functionality.
@brainkim As refs can be totally handled by the renderer rather than Crank core, I experimentally implemented this in Crank Native and it does appear to work (I was able to navigate from my rootView to my page via a ref, and thus we can see a nice Action Bar bearing the "Home" label):

I'm a bit bewildered by the pattern of resolving a Promise with this.refresh(), then yielding a <Copy/> inside a while(true) loop. For one thing, I wouldn't have been able to guess any of that, and for another, does this truly run synchronously? i.e. from the first line of the Greeting function all the way to my rootView.navigate() call, does the event loop remain inside this function?
Haven't looked deeply into crank yet, so I might be talking nonsense, but instead of having 4 different component declaration APIs (sync/async functions, sync/async generators), which adds confusion and potential future refactoring needs, is there a reason to not just use async generators for everything? (and maybe sync functions for dumb stateless+effectless stuff).
It would simplify the API, learning curve, maintenance, and possibly make crank leaner since it doesn't have to check for and handle 3 other cases now.
Never mind, found #28.
And I have to agree with @shirakaba, often times I need refs from deeper levels and without something like createRef() that just isn't possible (not conveniently at least).
@xkxx What I find cool is that youâve already gotten a basic handle of all four component types, their execution models, and some of the consequences. It took me the entire month of November 2019 to even begin to articulate what should happen with each. However, I disagree with your idea that things would be better if all four models behaved more consistently. I looked at each of the four function types and chose the behavior which would be the most useful for each, and I think this is more important than making them similar, because the behavior of all four function types is so wildly varying by nature. Sync generators couldnât yield continuously, because that would be cause an infinite loop. Async generators can, and we leverage this ability to do suspense-like fallbacks. And while async functions are technically stateful, sync and async functions share the characteristic that they donât have internal, user-defined state; if theyâre stateful, itâs because theyâre relying on state outside their scope. In short, I think that the function types are fundamentally different, so for them to execute differently as components is okay, and gives you clear reasons for choosing one function type over another.
The real mindfuck is when you think about how the different components interact as parents/children of each other. What happens when a sync function component returns an async component? What happens when a sync generator component yields an async component? What about when an async component returns an async component? All the permutations can make your head spin. This might trouble you, but the behaviors differ: sync functions will immediately pass updates to child async components, while sync generator components will wait for their async children before resuming. I chose this behavior specifically because of the problem of refs. It would be confusing if a sync generator resumed but its children had not rendered to the page yet, because they were async. What should the yield resume with in that case? undefined feels bad, but resuming with a promise felt just as bad, insofar as sync generator components have no way to await this promise. I think having sync generator components adopt the asynchrony of its yielded children is important for use-cases such as higher order components.
Which brings me to refs. One problem with callback-based refs is that developers have an implicit assumption that ref callbacks happen non-optionally. In other words, while they might expect an event callback to fire zero to many times, they usually assume that ref callbacks fire at least once, and often that the ref callback fires synchronously. However if a sync generator component has async children but is unmounted, the ref callback may never fire, and if the sync generator component transcludes children with refs into a child component, because that component controls when or even if the children will be rendered, the ref may fire differently that the developer expects:
function *MyComponent() {
let ref = (el) => {
// when does this callback fire???
};
while (true) {
yield (
<ChildComponent>
<div ref={ref} />
</ChildComponent>
);
}
}
If you donât know what ChildComponent is beforehand (as you wouldnât in the case of higher-order components) you have no way to understand when the ref callback will fire. What I like about the generator-based version of refs, where theyâre passed back into the component, is that a notion of linearity is preserved. The ref is necessarily available only after the component has finished rendering.
I do agree with you that it sucks that you canât run post-hooks after sync generator components, and maybe we could have an API for this like this.postCommit or something (we gotta bikeshed the name). Callback-based ref props are also relatively easy to do; I just want to be careful about it (the React refs API has gone through many successive iterations and maybe we can just solve it once without having to change it too frequently).
@brainkim What do you think of my approach to refs above (based on prop-setting rather than being callback-based)?
@shirakaba Me no likey đ
I think the React useRef API never made sense to me, and if you do decide to go with an element-specific ref approach a callback prop makes the most sense, because at least in that case you can put code in the callback to know when the ref is actually ready. But if you have thoughts to the contrary Iâm willing to hear them out.
@brainkim thanks for the long and thoughtful reply!
Re: refs and post-render hooks - it would be great to have a way to use them in non-async generator components. I like the this.postCommit idea, and I wonder if we can leverage it for refs. My strawman proposal: guarantee that refs will be ready when postCommit is called. (This echoes the notion of linearity that you talked about.) Refs should only be accessed in postCommit, and this allows refs to be modeled as properties.
Re: four models - I like that the four models have distinct expressive powers, and their behaviors come intuitively from native JS function semantics. My critique is that the distinctiveness of the four models
Additionally, sync generator and async generator components are visually similar but have vastly different behaviors, and that was confusing to me initially and I'm worried the subtlety would be a source of bugs in real-world apps. Both typically contain an overall for (props of this) { ... yield ...} structure, but one stops at yield and the other at for. I understand that it's difficult to fix on a framework level. One way is to not have sync generators altogether (https://github.com/bikeshaving/crank/issues/28), and the other is to establish a convention (potentially with the aid of a linter) that yield must be the last statement of the for loop in sync generators, so that we can pretend that both sync and async generators stop at for.
@brainkim I fully agree on your thoughts about refs, and that callbacks the most natural. In React you need the callback refs instead of createRef or useRef for some scenario's. A component could even choose not to render its' children, so there are no guarantees about when/if the callback will fire.
Regarding your example
function *MyComponent() {
let ref = (el) => {
// when does this callback fire???
};
while (true) {
yield (
<ChildComponent>
<div ref={ref} />
</ChildComponent>
);
}
}
Is it a problem that you don't know when the callback fires?
I fail to imagine a component where this could cause problems. Do you know of a use case where this is problematic?
Instead of copying React's way of doing this I think coming up with a model that fits inside Crank might be worth brainstorming over. The yield model has issues in sync generators, so what are some alternatives?
this.querySelector - This could work like the regular DOM query methods but be scoped to the root node last yielded. This wouldnt have to wait for the generator to resume, it could be used synchronously inside of callbacks/methods in the component.this.getNode('some-name') combined with <input crank-id='some-name /> - Crank could keep a map, flat or nested, that it maintained with the result of the last yielded render.These could actually co-exist!
The fact they they might be undefined because of un-finished async children isn't an issue, its actually an indicator of the fact that they haven't mounted.
Starting in 0.2, this issue should be resolved by the introduction of the schedule method, which takes a callback which is passed the rendered values as its first argument. The schedule method fires synchronously when the component has rendered, so you can write code like this:
function *FocusingInput(props) {
for (props of this) {
this.schedule((input) => input.focus());
yield <input {...props}/>;
}
}
function *FocusingInput(props) {
this.schedule(() => this.refresh());
const input = yield <input {...props} />;
for (props of this) {
input.focus();
yield <input {...props} />;
}
}
Refer to the updated guide on accessing rendered values for more information.
In addition, Crank now also implements the crank-ref prop, which works similarly to React callback refs, except we always âforwardâ refs because there is no such thing as a component instance or imperative handle.
Finally, in response to @kyeoticâs suggestion, you can now access a componentâs rendered value synchronously and immediately via the contextâs value computed property. This probably shouldnât be used in regular components, but you can probably use the value prop with one of the strategies described in the reusable logic guide to write a querySelector utility in user space. I didnât want to implement something like querySelector directly in the core because it felt too DOM-specific.
Let me know if this doesnât satisfy a use-case you were thinking of. Closing for housekeeping purposes.
Most helpful comment
Though it may seem like âgiving upâ to go back to old patterns, would simply re-using Reactâs approach of:
... solve things at all?
Iâm not accustomed to generators, but I feel like passing in an object (
myRef) to be populated upon the moment of the renderer performing props-setting would allow the ref to be populated before suspension of the function.Remember that many library consumers will want refs to not just the most basal component in the tree, but nested ones too, and I can only see something like this enabling that functionality.