facebook / jsx

The JSX specification is a XML-like syntax extension to ECMAScript.
http://facebook.github.io/jsx/
1.96k stars 133 forks source link

Add proposed fragment changes #93

Closed clemmy closed 6 years ago

clemmy commented 6 years ago

This is the JSX spec change for supporting <></> fragment syntax in JSX. One of the tougher design decisions on this spec was whether to go with a new node type for fragments (e.g. JSXFragment, or re-use the existing JSXElement node with an additional isFragment field. I opted for the latter because:

To illustrate the last point, I've written forks of babel, babylon, and eslint-plugin-react to prototype, and it turned out to have very few LOC.

Some example scenarios: < ></> // success

<></> // success

< attrib></> // fail

<>hi</> // success

<><span>hi</span><div>bye</div></> // success

<></something> // fail

UPDATE Spec has been updated to introduce new node type JSXFragment instead of re-using JSXElement.

https://github.com/facebook/jsx/issues/84

sebmarkbage commented 6 years ago

The authoritative syntax spec is the README.md file. The AST.md file is just a recommended form to be used by parsers but not required.

This needs to be defined fully in README.md without referencing AST.md and currently it doesn't.

In the actual syntax spec, we don't have such a flag precedence so I think it should be its own entry point. I.e.

JSXFragment:

clemmy commented 6 years ago

That makes sense. Do you think it'll be nonsensical to take out the isFragment flags and simply identify the fragments with JSXIdentifier whose name is ''? I guess that would still have the constraints to name being '' and attributes being [] though.

On another note, do you know how closely implementations follow specifications? I'm just wondering if I'll need to re-write my forks to follow the new spec for the first upstream into Babel.

sebmarkbage commented 6 years ago

They follow reasonably close and we try to adjust either the implementation or the spec to align whenever they diverge.

clemmy commented 6 years ago

@sebmarkbage Also, why interface JSXFragment <: Node instead of interface JSXFragment <: Expression, similar to how JSXElement is currently?

sebmarkbage commented 6 years ago

Ah, yea JSXFragment should probably be Expression. You're right.

clemmy commented 6 years ago

@sebmarkbage It seems a bit unnecessary to have JSXOpeningFragment and JSXClosingFragment since those always represent <> and </> in code. What do you think of just having JSXFragment, with the grammar being: <> JSXChildrenopt </> ?

sebmarkbage commented 6 years ago

I think in the spec it is enough to just have JSXFragment. The reason you want to also have the OpeningFragment and ClosingFragment in the AST is so that you can associate meta data such as start and end column/line location information with each of those separately.

clemmy commented 6 years ago

@sebmarkbage Ready for review again

sebmarkbage commented 6 years ago

cc @syranide @RReverser @hzoo @loganfsmyth

syranide commented 6 years ago

We're leaving out keying for the moment? Or is there an intended alternate mechanism (special function)?

Keying is kind of important for a bunch of use-cases so I think it's important to at least have an "answer" for when the question/issue is posted.

clemmy commented 6 years ago

You bring up a good point @syranide. I think that key-ing a fragment is a rare enough use case that if you really wanted to do it, you would simply not use the shorthand syntax (<></>) for it, but be more explicit and use what it de-sugars to (e.g. <React.Fragment key={...}> or <#fragment key={...}>). What do you think?

syranide commented 6 years ago

You bring up a good point @syranide. I think that key-ing a fragment is a rare enough use case that if you really wanted to do it, you would simply not use the shorthand syntax (<></>) for it, but be more explicit and use what it de-sugars to (e.g. or <#fragment key={...}>). What do you think?

E.g. any time you have a conditional with multiple different return-values that shouldn't be reconciled, they should be keyed. So I suspect it isn't as rare as one thinks, simply because most cases goes unnoticed and is only rarely done for that reason (thus; yes rare, but needs an official story). React by chance reconciling correctly or the incorrect behavior not being visually indicated/broken. I've seen a few such posts, where they have been utterly confused because it "usually works" and "looks right".

But it's also interesting to think about this case:

foo() {
  return <>...</>;
}
bar() {
  return <>...</>;
}
render() {
  return <div>
    {cond ? foo() : bar()}
  </div>;
}

What if you don't want to be reconciled, should you as good practice key the fragments in foo/bar or should you apply the key somehow in render?

Also I wouldn't think it desugars to any of your examples but rather a pre-keyed array (or special React-method). If it desugars to something "sensible" that seems like an ok balance, but I don't think so?

PS. I don't want to stall the proposal. Keying, however it ends up being done, should be well compatible with this proposal.

sebmarkbage commented 6 years ago

I think the idea is that the official story is <Fragment key="...">...</Fragment> and <>...</> is short hand for <Fragment key={null}>...</Fragment>. Of course, you're also free to properly key the individual items in the fragment which also avoids the issue.

clemmy commented 6 years ago

@syranide If I recall correctly, if the children are different inside foo's returned fragment and bar's returned fragment, then they shouldn't be reconciled. @acdlite may be able to shed more light on that. Also, as @sebmarkbage mentioned, it will be de-sugaring into something sensible.

Something to keep in mind during our discussion is that JSX is meant to be decoupled with React, and I think reconciliation is an implementation detail specific to React.

syranide commented 6 years ago

@sebmarkbage I'm assuming Fragment is a reserved keyword (or a special imported class) and not just the equivalent of a regular component class (i.e. opaque)?

@syranide If I recall correctly, if the children are different inside foo's returned fragment and bar's returned fragment, then they shouldn't be reconciled.

Only if the component classes are different, e.g. if they are two different buttons (save and discard) of the same class then it could have bad side-effects when one gets reconciled with the other.

Something to keep in mind during our discussion is that JSX is meant to be decoupled with React, and I think reconciliation is an implementation detail specific to React.

EDIT: Yeah of course, but how well it works for React seems like a good litmus test.

clemmy commented 6 years ago

@syranide Fragment will be an opaque component class exported under the React namespace.

clemmy commented 6 years ago

Thanks @sebmarkbage for merging my PR! I'll update my forks to have the correct implementation of the fragment syntax, and try to get it upstream into Babel/Babylon. :)

thysultan commented 6 years ago

Something to keep in mind during our discussion is that JSX is meant to be decoupled with React, de-sugars to (e.g. or <#fragment key={...}>)

How would this affect JSX in a non-React context, i.e with the pragma /* @jsx h */?

The #fragment or some kind of global symbol better favors inter-op with non-React libs.

clemmy commented 6 years ago

@thysultan For React, we're planning to have React.Fragment export a string, '#fragment', but we're not finalized on this yet. Inter-oping with non-React libs is definitely something we take into consideration, thanks for bringing it up! You can check out our discussion and progress here: https://github.com/facebook/react/pull/10783

uniqueiniquity commented 6 years ago

@clemmy It may not matter, but it seems like your changes to the AST and the README disagree? AST.md permits a fragment as a child of an element or another fragment, but the README doesn't. I saw the comments above that the README should be the authoritative source but I just wanted to check.

clemmy commented 6 years ago

@uniqueiniquity Ah, I think you're right - Are you referring to adding it over here:

JSXChild :

JSXText JSXElement { JSXChildExpressionopt }

uniqueiniquity commented 6 years ago

Yep, that's what I meant.

clemmy commented 6 years ago

Thanks for the catch!

geoffreydhuyvetters commented 6 years ago

Is this already released or do you have a version ETA? I love this :)

clemmy commented 6 years ago

Hey @duivvv. We're going to be officially releasing this with a blog post next week. 😄