-
Notifications
You must be signed in to change notification settings - Fork 131
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
Investigate implicitly adding sugar for JSX Completion Values #86
Comments
A bit odd but interesting example: render() {
if (this.state.invalid) {
<Message>{this.state.invalid.text}</Message>
}
<Button></Button>
} Becomes a fragment. |
@bmeck I was writing a reply to this in the other issue but apparently I was no longer allowed to. Anyway, there's a fundamental problem with having all children be some kind of "implicit return" as you showcase above. It's a super interesting concept (it's insanely flexible too), but it breaks implicit indices (and thus reconciliation). This means that in your example IMHO one of the not really well appreciated aspects of JSX today is that you are essentially defining a "fixed structure" for children, all children are put in predefined slots, and then each slot can have different values (but whenever it is more than one value, you need to key it, because the implicit index is unreliable). This is what gives rise to reliable implicit indices and that's an extremely useful thing. I'm not sure if this explanation makes any sense 😄 |
This is an investigation! Lets go over those problems. This also reminds me of #84 (comment) |
Just a note about a variation of your example: render() {
if (this.state.invalid) {
<Message>{this.state.invalid.text}</Message>
}
} Will have one of two behaviors when "this.state.invalid" is false.
Bonus, what happens if you do |
if (cond) {
<B foo />
} else {
<B bar />
} Will not reuse the for (...) {
<B foo={bar} />
} They will all have the same key, perhaps it can fall back to index, but that is a problematic behavior for keys in general I think. You could also just warn when the keys conflict in this case as we already do I guess. The bigger problem I think comes from calling functions and other kinds of less straight-forward uses: function foo(bar) {
<B bar={bar} />;
}
<>
(cond ? foo(1) : null)
foo(2)
</> This has the same problem as in my earlier comment, when you re-render that it will either require explicit keys (tedious and weird, because you have to make foo take a key) or the implicit indices will break when you re-render. These are just the obvious off the top of my head. I'm quite sure warnings cannot be emitted for most of these as the false-positives would be overwhelming. EDIT: JSX today behaves logically and when you're providing dynamic arrays it's easy to detect and issue warning for lack of keys, I think this intuitive behavior is imperative. But it seems it doesn't translate over to "implicit returns" without making all child-expression semantics JSX-specific too, possibly. |
Was this a mis-copy? It is missing As it stands we should probably throw an error or warning since the completion value contains a non-JSX value of
The statement in the original issue is only that you combine We should clarify that non JSX literals such as a number are not combined. We can also further clarify that only normal completions are combined which would prevent the
I would leave this as is, automatically deduping should be a separate issue.
The for loop only has a completion value of the last iteration. So you would only end up with one
Yes, this idea does not get rid of conflict resolution.
I might not be understanding this well enough. Is there something that forces "the implicit indices will break when you re-render" to be true? |
fixed up original issue text . |
Ok, wait. I assumed this was mostly a continuation of the previous issue and just glossed over your text. My bad. If it's just about adjacent JSX elements then these are my thoughts on that: #84 (comment) But IMHO that feature is largely irrelevant if there is larger discussion. It doesn't change anything, it's just sugar. EDIT: It is also error-prone and confusing because arr.map(item => {
if (Date.now() % 2) <Odd></Odd>
else <Even></Even>
}); Sure, like I mentioned in the previous issue, implicit return is a possibility (ASI is an issue though), but again, it's just a cosmetic thing, you could do this via do-expressions too. So you can argue the merits of it on its own, but personally I'm not a fan of it, it is unexpected and breaks with how JS otherwise works. It is also somewhat dangerous as it can hide code errors. Anyway, that's my personal opinion. 🤷♂️ |
The ASI is an issue that is solved the same way as other ASI issues are solved, by adding a semicolon. Right now it fails to parse which is much better than ASI issues like: return
{};
One without the other isn't very valuable, the value is in combining the 2 pieces proposed here. The do expression equivalent can be exceedingly verbose for some things like: render() {
if (this.state.invalid) {
<Message>{this.state.invalid.text}</Message>
}
<Button></Button>
} render() {
return [ // ASI hazard if on new line as well...
...do { // BIKESHED (we have not specified in this thread if this *always* vs sometimes generates an index)
if (this.state.invalid) {
[<Message>{this.state.invalid.text}</Message>]
}
else []; // this branch doesn't translate cleanly
},
<Button></Button>
];
} |
Well ASI is an official feature of JS and writing without colons is valid coding style, you solve return with parenthesis. So JSX should at least have a sensible solution to it. "Just add a colon" doesn't sound OK to me given that from looking it at it you would expect it to compile and someone not intimately familiar with it would be totally stumped.
I don't follow. render() {
if (this.state.invalid) {
<Message>{this.state.invalid.text}</Message>
}
<Button></Button>
<Button></Button> // added second button to actually showcase adjacent JSX elements
} If we assume that the fragment syntax render() {
if (this.state.invalid) {
return <Message>{this.state.invalid.text}</Message>
}
return <>
<Button></Button>
<Button></Button>
</>
} Sure enough it is different, but your example isn't exactly transformative. It's just more concise. I personally also find it confusing, from looking at your example I would assume that the buttons would be rendered no matter what (i.e. execution does not stop on a JSX element), because it looks like a templating language and that's how I would say templating languages work. It also seems like you've mischaracterized how it would look with do-expressions: render() {
return do {
if (this.state.invalid) {
<Message>{this.state.invalid.text}</Message>
} else {
<Button></Button>
}
};
} ... Or are you saying that the button IS rendered in your example, regardless of the invalid state? Because that is a whole different beast and we're back at the problems of #86 (comment), #86 (comment), #86 (comment). |
The completion value of the |
@bmeck Maybe it's just me or I don't fully understand the purpose of "phrasing it that way". If we take the original example as a starting-point: render() {
if (this.state.invalid) {
<Message>{this.state.invalid.text}</Message>
}
<Button></Button>
<Button></Button>
} Then it should translate to something akin to the following: render() {
if (this.state.invalid) {
push(<Message>{this.state.invalid.text}</Message>)
}
push(<Button></Button>)
push(<Button></Button>)
} Right? |
I should just clarify exactly instead: render() {
let out = [];
if (this.state.invalid) {
out.push(<Message>{this.state.invalid.text}</Message>)
}
out.push(<Button></Button>)
out.push(<Button></Button>)
return out
} EDIT: Are we on the same page or am I misunderstanding you in some way? |
@syranide kind of? Though you need to be careful about some things like loops that do not have completion values per iteration and |
@bmeck Here's where I'm confused, we're talking about functions, no normal statements inside a function has a completion value. So the above examples don't make sense? It seems like you're assuming some kind of do-expression semantics that apply to all functions? |
@syranide In JS, statements have completion values. This can be seen in various RuntimeSemantics of the spec regarding evaluation such as https://tc39.github.io/ecma262/#sec-block-runtime-semantics-evaluation returning blockValue. |
@bmeck Sure, but not in any meaningful way right, you cannot access these completion values other than via eval...? So AFAIK they only seem to exist for the purpose of more useful interactive prompts. It's not actually something you can use outside of that. |
Yes, it would not combine. The 2 JSXElements would combine and become some form of fragment ( I don't see how this would cause things to not work since we are only combining adjacent JSXElements. |
Sorry, my mistake. Updated reply below: 😄 And if it is those completion value semantics you're piggy-backing on then I don't see how it would work in practice. <A />
let bar = 0;
<B foo={bar} /> Would return only the last |
Also, trying to transpile adjacent completion values in this way sounds like a proper nightmare if you ask me, not to mention the runtime performance overhead (hmm, perhaps it isn't bad actually). |
I agree, that example is confusing but easy to explain why it fails. I am not sure how realistic this kind of coding is to become commonplace since it doesn't have meaningful effects in current compilation chain.
Since JSXElement is a compile time only construct you don't need to do it at runtime. We are not combining the results of functions. I should clarify that. |
Ok, now I get what you're getting it. Took me a while. <div>
Foo
<A />
Bar
<B />
</div>
Anyway, if you assume we get rid of text-nodes and forget about exact completion values semantics, then you could allow: if (cond) {
<A foo />
} else {
<A bar />
}
if (cond2) {
<B />
}
<C />
let bar = 0;
<D foo={bar} /> Which could compile to the equivalent of: let out_1 = null
if (cond) {
out_1 = <A foo />
} else {
out_1 = <A bar />
}
let out_2 = null
if (cond2) {
out_2 = <B />
}
let bar = 0;
<>
{out_1}
{out_2}
<C />
<D foo={bar} />
</> Which might kind of make sense, but understanding the exact semantics of this is probably too complicated for any layman, and I'm not sure how you would deal with nested ifs right now, and you would probably have to automatically put a fragment around for-loops (which is problematic too). |
I would want
Absolutely not, |
We could say that |
I don't see the merit of completion value semantics for JSX, it seems artificially limiting and unintuitive. But perhaps I'm just not seeing the light right now, anyway, I don't mean to discredit this issue if you believe there's value in it. I just don't think I can be of much help for the time being. |
It matches do expressions so I have absolutely zero desire to move on my statement that loops must produce a singular value. |
@bmeck Hmm, after thinking about it a bit more I think I'm starting to see what you're getting at. But I'm not sure what real-life problem you would really be solving that couldn't be rewritten to something equally good without these semantics. Can you think of any real-life examples where this would actually make the code significantly better? |
render() {
if (this.state.invalid) {
<Message>{this.state.invalid.text}</Message>
}
<Button></Button>
} Is actually super-weird to me now that I think about it. It creates a fragment, where message becomes combined with button. render() {
if (this.state.invalid) {
<Message>{this.state.invalid.text}</Message>
}
foo();
<Button></Button>
} However that would So IMHO, if you remove that I'm not sure what value there is in having completion value semantics vs just straight-up implicit return. Completion value semantics just seem more error-prone whereas implicit return would actually allow you to cut down on the clutter. What am I missing here? |
Making this work only within fragment syntax of That would complicate things since: render() {
<>
if (this.state.invalid) {
<Message>{this.state.invalid.text}</Message>
}
<Button></Button>
</>
} Is problematic like you said due to text. This might make the things a bit odd still since it is upon completion value instead of when they are evaluated. I think that adjacency is pretty simple, but am open to talk about things.
People will need to learn about completion values due to |
@bmeck I'm busy with other things this weekend, but I've slept on it and have some more thoughts on this. |
Do-expressions seem simple in comparison to me, basically just return the last statement and the do-expression syntax itself means that it will raise an eyebrow. But these are implicit do-expressions (don't know where they start or end, unless It also seems confusing that I can type I find it hard to get a really good grasp of how this would work out in reality. It's one thing to show an example where it looks nicer, but this syntax would have to replace the JSX syntax as we know it, i.e. it has to do everything well, not just a handful of things. The implicit indices will become very problematic I believe. So just to take a simple example. Today I can do: render() {
if (cond) {
return <Foo />;
}
return <Bar />;
} render() {
return (
<div>
{cond ? <Foo /> : <Bar />}
<FooBar />
</div>
);
} render() {
let elem = <FooBar />;
return (
<div>
{elem}
</div>
);
} createFooBar(...) {
return <FooBar ... />
}
render() {
return (
<div>
{createFooBar(...)}
<Message />
</div>
);
} As far as I can tell, even these very basic examples would not read very well when written in the proposed syntax. If you just take the last example as a starting-point, I'm assuming it would look like this: createFooBar(...) {
return <FooBar ... />
}
render() {
return (
<div>
createFooBar(...)
<Message />
</div>
);
} Which is fair enough, nothing strange. But if you now actually use the syntax further it becomes really hard to make sense of it: createFooBar(...) {
return <FooBar ... />
}
render() {
return (
<div>
let foobar;
fizzbuzz = doSomething() // this seems hugely problematic
createFooBar(fizzbuzz...) // what if this doesn't return an element
<Message />
</div>
);
} Removing adjacency-rules in-favor of <div>
<A /> // wrong!
<B />
</div> <div>
<>
<A /> // right!
<B />
</>
</div> I find it really hard to wrap my thoughts into a concise argument at this point, but at this point I strongly feel that the current way JSX works really is the best approach (as a starting-point), it mostly just lacks good constructs for flow control (e.g. implicit do-expressions inside brackets). I think it could also greatly benefit from supporting statements in some way, (obviously, that's what this issue is about). Unless I'm mistaken, implicit do-expressions inside brackets solve this too. createFooBar(...) {
return <FooBar ... />;
}
render() {
return (
<div>
{
let foobar;
fizzbuzz = doSomething(); // this seems hugely problematic
<>
{createFooBar(fizzbuzz...)}
<div />
</>;
}
{
if (special) {
return <Fizz />; // this is kind of weird though, but we nice to be able to do
}
if (cond) {
<Foo />;
} else {
<Bar />;
}
}
{value}
<MoreStaticElements />
</div>
);
} Perhaps I've overlooked something important here, but implicit do-expressions inside brackets seems like a far favorable solution (although I'm not entirely happy with some details of the above code). Because it retains the "pre-defined slots/indicies" which is hugely important for intuitive implicit indicies, while also making the more complex expression/statements style opt-in rather than default behavior. If we ignore TextNodes, then the brackets above may be seen as just superfluent and annoying (which I guess is an important part of this issue as well). But it's not entirely true if we want to preserve intuitive and meaningful implicit indicies (I think...) and they do add boundaries (you can construct elements independently inside the same parent, although debatable how useful that really is in general). But we theoretically should be able to remove them and retain similar behavior if we go with a behavior similar to one of my earlier comments #86 (comment). That each JSX-element essentially pushes to stack (with a predefined implicit key). Does any of this make any sense to you? 😄 |
Rephrasing of #85
Examples work in part assuming #39 is also necessary for this. Glad to be proven wrong on that front.
Examples are also using the syntax from #84 for fragments, but assume this is just sugar for now.
I would like to investigate implicitly combining adjacent completion values of statements that resolve to JSX expressions.
Is an example where implicit returns struggle. However, if instead we treat the completion value of statements as the mechanism for adding sugar we can do a few things.
Combine adjacent JSX completion values into fragments
This works fairly well, but can have some odd syntax going on with semicolons and blocks:
Would be equivalent to both:
And
Implicit return for JSX completion values of functions
The function has a completion value that is JSX. I would make a warning or error if mixing types that are not JSX and JSX here:
The text was updated successfully, but these errors were encountered: