-
Notifications
You must be signed in to change notification settings - Fork 132
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
Add proposed fragment changes #93
Conversation
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:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would propose that you explore:
interface JSXFragment <: Node
interface JSXOpeningFragment <: Node
interface JSXClosingFragment <: Node
AST.md
Outdated
@@ -73,12 +73,13 @@ Any JSX element is bounded by tags — either self-closing or both opening a | |||
```js | |||
interface JSXBoundaryElement <: Node { | |||
name: JSXIdentifier | JSXMemberExpression | JSXNamespacedName; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This type annotation is now incorrect or you need to describe what should be in here since fragments don't have an identifier nor member expression nor namespace name.
If isFragment
is true this is always null
, if isFragment
is false then this is always not-null
.
That's an indication that this shouldn't use a flag but its own interface.
AST.md
Outdated
@@ -73,12 +73,13 @@ Any JSX element is bounded by tags — either self-closing or both opening a | |||
```js | |||
interface JSXBoundaryElement <: Node { | |||
name: JSXIdentifier | JSXMemberExpression | JSXNamespacedName; | |||
isFragment: boolean; | |||
} | |||
|
|||
interface JSXOpeningElement <: JSXBoundaryElement { | |||
type: "JSXOpeningElement", | |||
attributes: [ JSXAttribute | JSXSpreadAttribute ], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This type is also not describing the constraints since attributes will always be an empty array.
AST.md
Outdated
} | ||
|
||
interface JSXOpeningElement <: JSXBoundaryElement { | ||
type: "JSXOpeningElement", | ||
attributes: [ JSXAttribute | JSXSpreadAttribute ], | ||
selfClosing: boolean; | ||
selfClosing: boolean; // if this is true, isFragment must be false, and vice-versa |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment is not fully describing the type constraints. It's just ad-hoc. Declaring a new interface will avoid that problem.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ooops. Meant to request changes.
That makes sense. Do you think it'll be nonsensical to take out the 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. |
They follow reasonably close and we try to adjust either the implementation or the spec to align whenever they diverge. |
@sebmarkbage Also, why |
Ah, yea JSXFragment should probably be Expression. You're right. |
@sebmarkbage It seems a bit unnecessary to have |
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. |
@sebmarkbage Ready for review again |
README.md
Outdated
@@ -60,6 +61,10 @@ JSXClosingElement : | |||
|
|||
- `<` `/` JSXElementName `>` | |||
|
|||
JSXFragment : | |||
|
|||
- <> JSXChildren<sub>opt</sub> </> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should use the ` character around each token with a space between.
- `<` `>` JSXChildren<sub>opt</sub> `<` `/` `>`
So the formatting comes out like this:
<
>
JSXChildrenopt<
/
>
The point of this is because we build this on top of the normal tokenizing in JavaScript. These individual tokens already exist but there's no <>
token or </>
token so when building a parser, you have to parse these individually. It also means that it is ok to have whitespace or even comments(!) between them.
E.g. this is valid:
let fragment = <>
< // comment
/ /* another comment */ >;
ok. lgtm. Let's get some more stakeholder looking at this before merge.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems reasonable to me.
AST.md
Outdated
@@ -126,14 +126,40 @@ interface JSXText <: Node { | |||
JSX Element | |||
----------- | |||
|
|||
Finally, JSX element itself consists of opening element, list of children and optional closing element: | |||
JSX element itself consists of opening element, list of children and optional closing element: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A JSX element consists of
maybe?
AST.md
Outdated
JSX Fragment | ||
------------ | ||
|
||
JSX fragment itself consists of an opening fragment, list of children, and closing fragment: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above. Feels like the grammar is weird in these.
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. |
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 ( |
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 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. |
I think the idea is that the official story is |
@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. |
@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)?
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.
EDIT: Yeah of course, but how well it works for React seems like a good litmus test. |
@syranide Fragment will be an opaque component class exported under the React namespace. |
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. :) |
How would this affect JSX in a non-React context, i.e with the pragma The |
@thysultan For React, we're planning to have React.Fragment export a string, |
@clemmy It may not matter, but it seems like your changes to the AST and the README disagree? |
@uniqueiniquity Ah, I think you're right -
|
Yep, that's what I meant. |
Thanks for the catch! |
Is this already released or do you have a version ETA? I love this :) |
Hey @duivvv. We're going to be officially releasing this with a blog post next week. 😄 |
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 existingJSXElement
node with an additionalisFragment
field. I opted for the latter because:JSXElement
in terms of the AST (e.g. it contains children, openingElement, and closingElement)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-usingJSXElement
.#84