facebook / jsx

The JSX specification is a XML-like syntax extension to ECMAScript.

Home Page:http://facebook.github.io/jsx/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Proposal: Namespaces for passing nested data structures

geelen opened this issue · comments

This is following on from a chat in person with @gaearon, and I think is different enough from #66 to warrant its own discussion, so here goes:

I'd like to propose that namespaced attributes generate a simple nested group of attributes

<Comp normal="prop" ns:foo=bar ns:baz=true />

compiles to

createElement(Comp, {
  normal: "prop",
  ns: {
    foo: bar,
    baz: true
  }
})

This to me is a really natural extension of the existing prop semantics, shouldn't conflict with anything proposed in #65 but it also allows a number of nice design possibilities.

React

// From #66
<Foo react:key="foo" react:ref={callback} prop="hi" />

key and ref are now no longer simply "special", they're explicit metadata passed to React. It's a fairly big change to React's core API but I don't see any reason to prevent a codemod doing the bulk of the conversion automatically.

Styled Components

// Styled components use case
const Button = styled.button`
  color: ${ sc => sc.primary ? 'white' : 'palevioletred' };
`

<Button type="submit" sc:primary=true />

This is one of the big pain points of designing a library like Styled Components (styled-components/styled-components#439) where we're trying to encapsulate the styling props and the HTML attributes of an element in a single unit. This would make the two types of attributes totally distinct, but without breaking the expressiveness of the component API.

Passthrough props

// Less need for object rest for masking spread properties
const Link = props => {
  const { x, y, z } = props
  return <a {...props.inner} />
}

<Link x y z inner:href="/" inner:target="_blank" />

This comes up a lot (e.g. Hacker0x01/react-datepicker#517 (comment)) where {...props} ends up transferring more properties down than is desirable. The above lets the creator of a component explicitly allow "inner" props to be forwarded on.

Alternatively, if a Link component has a fairly small API and passes on most props, wrapping up all the link-specific props into a single namespace and forwarding on the rest.

// Masking using object rest is slightly simpler too
const Link = ({ link, ...props }) => {
  const { x, y, z } = link
  return <a {...props}/>
}

<Link link:x link:y link:z href="/" target="_blank" />

I don't propose changing anything else about namespaced props, they should work exactly like non-namespaced ones. It simply gives a component author the ability to partition the API into owned and non-owned properties, in a way that's explicit, named, and simple enough that static type inference/checking should be able to be preserved.

What do you think?

Isn't it wrong to repurpose namespaces for objects? Why not use inner.href which would reflect the syntax used for objects in JS.

key and ref are now no longer simply "special", they're explicit metadata passed to React. It's a fairly big change to React's core API but I don't see any reason to prevent a codemod doing the bulk of the conversion automatically.

Now react is special instead. Without special-casing react in JSX this will also cause an additional object allocation, but special-casing intuitively sounds like a bad idea because JSX is not React. Anyway, that may just be a matter of naming convention.

Passthrough props

<Link x y z inner:href="/" inner:target="_blank" inner:foobar="bizbar" />
<Link x y z inner={{href:"/", target:"_blank", foobar:"bizbar"}} />

The already supported syntax is shorter and also doesn't hide the fact that you end up allocating an object. Your syntax will also favor short names over descriptive names to avoid repetition. It also makes it difficult to refactor when objects become too big, i.e. when you sit there with 20 inner-props and you want to move them out into a shared/external object/helper, you now have to rewrite all of it. If you use the already supported syntax you can just copy-paste or use the entire existing expressiveness of JS to help you generate the object.

The syntax opens up for the possibility of redefining the semantics of the generated object (e.g. it is immutable and reusable if constant) which may be an interesting JSX optimization, but I doubt it would be meaningful in the bigger picture and the added user complexity is not to be taken lightly.

So personally I fail to see the benefit of this other than "it looks nicer"?

I really like this. It feels spiritually similar to what happens when you pass data attributes, as they get coalesced into element.dataSet.

What if my hypothetical 'super components' library wants to have it's special props under 'sc' too? Would styled-components have dibs on sc props? This is why XML namespaces work how they do, with a globally unique URI and a locally unique alias. I don't know how or if this can translate cleanly into the JSX world.

Its not a matter of dibs, it simply translates sc:primary to { sc: { primary: true }}. It's up to each component what it does with it. It doesn't have to be globally unique, only relevant to the component that you're targeting.

It looks like the spec supports namespaced tag and attribute names, but Babel says “Namespace tags are not supported. ReactJSX is not XML.”

Someone else suggested something similar, except with a . in an unrelated RFC of mine. I had a similar thing to say about the ns.key or ns:key syntax` to what @syranide said.

One problem I have with the css.width idea exactly as-is, is that it now takes a single character to de-optimize PureComponent. In JSX css.width="100px" looks like you are defining flat props, but in reality it's actually css={{width: "100px"}}, which creates a new object every render and doesn't allow PureComponent to work.

If we wanted that, IMHO we'd have to come up with some way to memoize the prop code in a way that avoids creating a new value when css.* is shallow equal.

I'd be happy with a namespace.prop syntax as well. I don't think this would need to deoptimise PureComponent at all, if namespaces were first-class JSX citizens then a caching strategy should surely be possible, for example:

<Button primary css.width="100px" attr.aria-label="Some button">

// Raw props used for cache-busting
const props = {
  'primary': true,
  'css.width': '100px',
  'attr.aria-label': 'Some button'
}

// Some richer object with aliases used for render method
this.props = Object.create(props, {
  css: { value: { width: props['css.width'] }},
  attr: { value: { 'aria-label': props['attr.aria-label'] }},
})

Babel says “Namespace tags are not supported. ReactJSX is not XML.”

Yeah but it can be added to the JSX spec and then Babel will adjust to support it. namespace:colon feels most right to me since it mirrors how (X)HTML looks, which is the origin of JSX.

Yeah but it can be added to the JSX spec and then Babel will adjust to support it. namespace:colon feels most right to me since it mirrors how (X)HTML looks, which is the origin of JSX.

  • JSX isn't xml, baring some exceptions that may change, prop names are JS properties like readOnly and onClick not html attributes. Using more XML-like syntax to refer to JS features we are adding is confusing.
  • namespace:prop also clashes, it uses XML-like syntax to refer to JS sub-objects like {css: { width: value }} which are not what you think of when you see the xml-colon style namespace syntax. Instead you think about namespaces like svg:, xlink:, etc... which, should someone somehow convince the JSX authors there is a good enough use case to include support for it in the spec (say a non-react library using JSX to author XML) won't be able to use the ns:attr syntax because it's been usurped by a different unrelated feature.

@probablyup I think you might’ve missed the part of the sentence before your quote:

It looks like the spec supports namespaced tag and attribute names

What's the process for getting this formalized? Getting it into Babel is the first step I assume?

I wrote a POF babel transform https://astexplorer.net/#/gist/27b6ec34ebd7036df149229f65bcc235/d8386fe502ceee0e1cb66cdb2b98fdfd26989c90 (it doesn't cover edge cases like spreads etc) fwiw I think that this is redundant and a bit abusing of namespaces :) Also I think that what can be done with a Babel plugin probably should stay in user land.

fwiw I think that this is redundant and a bit abusing of namespaces

Why's that? I strongly feel it's a primitive that the ecosystem would massively benefit from.

Also, thanks so much for taking a crack at the plugin code!

One tweak I might add to this strategy is prefixing generated namespaces with a character so they don't conflict with similarly-named props.

<Comp normal="prop" theme:foo={bar} theme:baz theme={fizz} />

createElement(Comp, {
  normal: "prop",
  $theme: {
    foo: bar,
    baz: true
  },
  theme: fizz,
})

Note the $ for the namespace collection. Could also use _ as it's another default-allowed variable.

That would disallow this, though:

<div style:color="red" style:border="1px solid black" />

How so?

<div style:color="red" style:border="1px solid black" />

createElement('div', {
  $style: {
    border: '1px solid black',
    color: 'red',
  },
})

And then you could do an inline style if you want too:

<div style:color="red" style:border="1px solid black" style={{ marginTop: 1 }} />

createElement('div', {
  $style: {
    border: '1px solid black',
    color: 'red',
  },
  style: {
    marginTop: 1,
  }
})

^ the use of style: namespace assumes that an HOC will be consuming that.

Basically a namespace is a different concept than an object - the prop representation is an object, but they're meant to be separate collections of props.

Why not just do this in some new wrapper/redux functionality:

mapEverythingToProps(state, parentProps){
   return {
       stateProps: state,
       parentProps: parentProps,
   }
}

const fc = (props) => {
   console.log({props});  //  {stateProps:{}, parentProps:{foo:'bar', x: 5}} 
   return <div>hi</div>
}

export default connect<>(
  null,
  null,
  mapEverythingToProps
)(fc);

and we render it with the usual:

<fc  foo={'bar'} x={5} />

the difference is that the connect actually intercepts parent props and actually allows you to namespace them. Of course this is more of a redux thing than a react thing.