Skip to content
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

Consider Using JsonML as Notation for ReactElement #2932

Closed
sebmarkbage opened this issue Jan 25, 2015 · 23 comments
Closed

Consider Using JsonML as Notation for ReactElement #2932

sebmarkbage opened this issue Jan 25, 2015 · 23 comments

Comments

@sebmarkbage
Copy link
Collaborator

DON'T GET TOO EXCITED, this probably won't work. This issue is more about documenting why we can't do it.

We could consider using JsonML as the notation for ReactElement.

This would make this JSX...

render() {
  return <div className="container"><FancyButton disabled /><span /></div>;
}

...which currently looks like this...

render() {
  return { type: 'div', props: { className: 'container', children: [
           { type: FancyButton, props: { disabled: true } },
           { type: 'span' },
         ] } };
}

...into a much nicer non-JSX declaration.

render() {
  return ['div', { className: 'container' }, [FancyButton, { disabled: true }], ['span']]
}

It would also align us with a spec which is not ours. Almost like a standard.

Children are Special

JsonML is designed in a way that children can be distinguished from other attributes. We don't really care about this property because we've already merged them, but we could undo that. NBD.

One problem is that you constantly need to slice off the children from the outer array as you're passing it into a component. This can become a performance problem.

key and ref

The problem with JsonML is that we don't have a way to attach custom attributes to the element, that are not props. I.e. key and ref. It is important that these are treated differently than other props because they should not be available to the component itself. Conceptually the parent is responsible for keying the element and changing the key should not affect the behavior of a component. Same for refs. It is also important that props can be transferred to a child without that affecting its behavior.

One possible solution would be to wrap the elements in another object:

renderButton(data) {
  return { key: data.id, element: [FancyButton, { disabled: true }] };
}
render() {
  return { ref: c => this.container = c, element:
    ['div', { className: 'container' }, this.props.data.map(this.renderButton)]
  };
}

It's still ugly. It also means that you now have to reason about two types of element (with or without wrapper). These children are also ambiguous in the first position of an attribute-less element.

Constant Destructuring

Any time you need to clone or reason about an element you need to destructure the system.

var [tag, props, ...children] = element;
return <tag {...props} foo={10}>{...children}</tag>;
var [tag, props, ...children] = element;
return [tag, {...props, foo: 10}, ...children];
return <element.type {...element.props} foo={10} />;
return { ...element, props: {...element.props, foo: 10 } };

Nested Arrays

We can't distinguish between a nested array of elements and just an element. This would only work if we enforced that children are always provided as flat lists and explicitly flattened as needed.

E.g. ['div', ...this.props.children, ['div', { className: 'extra-child' }]]

Flattening makes it very difficult to preserve keys though.

What About Allowing Both?

A component that receives a ReactElement from the outside should not need to reason about two different kinds of abstractions and use two different patterns for accessing the type or props. It is important that the React community doesn't diverge too far from each other.

@sebmarkbage
Copy link
Collaborator Author

cc @chenglou

@chenglou
Copy link
Contributor

Oh boy! Gets excited anyways

Here are some counterarguments, plus some other arguments on the pros of this.

  1. Non-JSX declaration: pretty much. Less React-specific tooling is good.
  2. Children are special: slicing off children is expensive with mutable arrays, yeah. But since we're (maybe?) considering persistent collections (or even your lazy splat/destructuring) this should be fine. key and ref, I agree. What about ['div', {key: 1, ref: bla}, props, children]? Probably not good for perf (can static analysis help here?), but the current ['div', {prop1: bla, key: 2, ref: bla2}, children] is fine too. Plus, it's more explicit, in this format, that the component shouldn't expect to receive key and ref in the object (we'll strip them before passing on the props obj and properly document this). It was harder to justify under the old div({key: 1, prop1: bla}) and still not that obvious with createElement I'd say.
  3. Constant destructuring: beside perf concern (which I pointed out above... The fact that it's lazy/persistent should help right?), I don't see the problem with that. It's much clearer and I prefer this over the cloneWithProps API. Smaller API surface and all. But if we really want, we could just make cloneWithProps use this destructuring under the hood. I don't see a problem here.
  4. Nested arrays: since the specs aren't set in stone (even if they are, we don't have to follow them closely) I propose the format [tag, props, childrenArray] instead. Also solves the problem of having to spread children in the previous destructuring point.
  5. What about allowing both?: nah lol

I don't think we have to stick with JsonML's specs too closely, if ours make more sense. For example, why is its style value a string? Why not an object like in React? Maybe you can influence the specs?

Now for the pros:

  1. (Ideally) React-agnostic render functions: if a library provides export function render(obj) => ['div', obj.a, bla], this is a win for future React-like libraries since that function doesn't depend on React. It's much more nuanced than that, I know, but this is a good start.
  2. Potentially dropping ReactChildren: we're not gonna implement every array-manipulating functions (like we did with count). We did say that, for perf reasons, we wanted to keep the underlying children data structure opaque (e.g. to ease the switch to linked list), but realistically I don't think this is happening, and the benefits of working with plain arrays optimized by engines are too big. A good example is the Select problem we've seen:
<Select><Option selected={true} /><Option /></Select>

This is weird because we're passing a prop to a child, then making the parent read into it to determine some stuff. The theoretically better way:

<Select selected={0}><Option /><Option /></Select>

But this is hard to manage and might get out of sync. Even better way (@jordan):

<Select items={[['Option', {selected: 0}], ['Option']]} />

In which case we have a good excuse of being able to read selected: it's nothing but an array, that we'll also happen to render. With the JsonML format, this come for free:

['Select', {}, ['Option', {selected: 0}], ['Option']]

Also easier for libraries like TransitionGroup to read into/manipulate children. I might slice off children for infinite scrolling or something.

  1. Killing the distinction between owner/parent tree: I haven't given much thoughts to the implication of this, but it might be nice. If I pass around a persistent children array, I wouldn't really own it; for all I know that array is always cloned before being mounted (myArray === myClonedArray).
  2. This is what the ClojureScript people already do: https://github.com/reagent-project/reagent#examples. Except they actually precompile that to React components to play nice with us. Won't have to anymore.

@gaearon
Copy link
Collaborator

gaearon commented Jan 28, 2015

This is weird because we're passing a prop to a child, then making the parent read into it to determine some stuff.

I see this use case very often in my code. I usually hesitate between keeping stuff as data (and array prop) or using children, and children wins because it looks clearer on consuming side, but makes it more painful for the component itself.

@chenglou
Copy link
Contributor

Yep. Not saying it's not possible with createElement but it feels definitely more excusable to directly read into normal collections.

I think that we should work with vanilla JS collections as much as possible (including their generation), and keep React at the edge only. createElement would be too dangerously convenient, because it allows us to cram new concepts into the arrays and still keep things superficially clean.

@chenglou
Copy link
Contributor

Oh and @sebmarkbage: children comparison becomes easier. You'll also get whatever optimization you want from Flow for free (e.g. pull immutable arrays outside to avoid allocations). There are probably a lot more [insert optimizations for normal arrays/tuples] that I haven't thought about.

@brigand
Copy link
Contributor

brigand commented Jan 29, 2015

If this can be worked out from a performance standpoint, +1. If we can get some convergence from other libraries by a standard or even shared conventions +1,000.

This does also make things like ReactRouter's use of JSX for describing routes more sane, and not tied explicitly to React.

['Select', {}, ['Option', {selected: 0}], ['Option']]

Did you mean to have the quotes around Select and Option, or are those supposed to be references?

@chenglou
Copy link
Contributor

No. Not sure why I quoted those.

@brigand
Copy link
Contributor

brigand commented Jan 29, 2015

Okay, thanks. Also for the sake of clarity and compat I think putting any metadata last makes the most sense, like an optional parameter to a function should usually go last.

['div', props, children, {key: 1, ref: bla}]

This makes handling this kind of data easier because you know data[1] is props, always. It also allows you to say 'this props doesn't look right, I should throw an error/warning' vs 'this metadata might not be intended for me'.

(edited: sorry, accidentally hit the submit key shortcut while typing)

@syranide
Copy link
Contributor

@sebmarkbage It seems to me that the current object-based representation should be preferable from a technical perspective... in addition to actually making sense if you try to operate on them i.e. elem.props.name, but elem[1].name???

Something to consider is supporting JsonML via run-time/static transformation just like JSX instead, except it would be JS-compatible so there shouldn't be any tooling issues for static transformation. Wrap all React JsonML in ReactJML(...) or whatever and if it has not been statically transformed you incur the (somewhat significant?) run-time overhead, if it has been statically transformed it's equal to JSX. No overhead and we get to choose the most suitable technical representation without considering it having to be especially human readable.

render() {
  return ReactJML(
    ['div', { className: 'container' },
      [FancyButton, { disabled: true }],
      ['span']
    ]
  );
}

JsonML seems like yet another DSL for something which should have it's own dedicated syntax. I don't think the solution to problems like this is inventing ways of making the code less ugly with magical arrays (just because arrays have minimal character overhead), it's understanding that view hierarchies require a syntax designed for it (that's why we have HTML to begin with) and it's time for languages to catch up.

@Raynos
Copy link

Raynos commented Feb 5, 2015

I've done some tooling with jsonml in the past.

I've considered jsonml to be [tagName, properties, children] with some special casing for text nodes.

I like the second library because it's a bunch of form helpers that are completely framework agnostic.

All I've implemented for jsonml is creating elements & html strings. zero diffing or patching.

I've since moved away from jsonml for a few reasons:

  • I got frustrated with the lisp problem ['div', [['div'], ['div', [['span'], ['span']]]]]. Lot's of trailing braces. I prefer hyperscript or something like it where it's h('div', [h('div'), h('div', [h('span'), h('span')])]) it's a small thing but it helps.
  • There is a large performance penalty of converting a JSONML data structure into a tree of virtual nodes. It's basically a full deep copy. You need some kind of optimized tree for the patch and diff algorithm to be efficient, JSONML cannot really be an intermediate format.

@chenglou
Copy link
Contributor

chenglou commented Feb 5, 2015

@syranide var [tag, props, children] = yourJsonML solves your first problem.
I'm not arguing whether JsonML looks nicer btw (I actually think it's fine. But like @Raynos said, unless you're a lisp guy using paredit the ]]]] might be a bit noisy). You can always make JSX compile to that in the worst case.

@Raynos good to know these exist. What do you mean by the perf cost? Can immutable collections mitigate this?

@syranide
Copy link
Contributor

syranide commented Feb 5, 2015

@chenglou Still not a nice way to do it, you wouldn't do it like that if it wasn't for JsonML. My point is that it seems weird to make JsonML the target, why not make JsonML compile to the best target instead? Just like JSX does.

@jimfb
Copy link
Contributor

jimfb commented Feb 5, 2015

It would also align us with a spec which is not ours. Almost like a standard.

❤️

@chenglou
Copy link
Contributor

chenglou commented Feb 5, 2015

@syranide yeah that's what clojurescript people do: https://github.com/reagent-project/reagent#examples

@Raynos
Copy link

Raynos commented Feb 5, 2015

@chenglou the internal representation of a virtual dom in both React & virtual-dom is not JsonML. This means you have to convert data structures, this includes a full deep copy basically.

In virtual-dom we avoid this with a h() helper that looks like JsonML but returns an instance of VNode which is the internal data structure used by virtual-dom. This reduces the amount of performance overhead during the render() phase.

@mrozbarry
Copy link

I just want to chime in with my 2 cents, and my experience with using react with coffeescript (and no jsx).

At the top of my coffee file, I import in the elements I plan on using:

{div, span} = React.DOM
MyOwnComponent = React.createFactory(namespace.MyOwnComponent)

My render methods end up looking like:

render: ->
  div
    myFirstProp: 'test'
    mySecondProp: 'foobar',

    span {},
      'Some content here'

    MyOwnComponent
      list: [1, 2, 3]

Which is suprisingly similar to JsonML, but with less clutter (at least in my opinion).

With this in mind, if this is only a readability issue, I think it's unnecessary to add another notation, since coffeescript enforces indentation which them clearly demonstrates parent/child relationships of elements.

I've never had to use JSX for any of my components, so JsonML is not necessary in terms of an alternative to JSX.

If JsonML is a necessity, it would probably be possible to just wrap that data coming in via native JS:

function jmlToNative(node) {
  return null if node == null;
  var elementName = node[0],
        elementProps = node[1];
  var element = React.DOM[elementName];
  var elementChild = (node.length == 3) ? jmlToNative(node[2]) : null;
  return element(elementChild, elementChild);
}

render: function() {
  jsonML = ['div', { className: 'container' }, [FancyButton, { disabled: true }], ['span']];
  return jmlToNative(jsonML);
}

I don't know what sort of performance hit this actual has

@aaronjensen
Copy link

There may be good reasons that it wouldn't work, but along these lines, what if we used strings instead of references for child components?

render() {
  return ['div', { className: 'container' }, ['FancyButton', { disabled: true }], ['span']]
}

This would obviate the need for shallowRender, and it would invert control for the components. You'd have to register your components centrally/pass them to React.render.

@danristea
Copy link

danristea commented May 7, 2016

I don't know about React's implementation in particular, but I've created a library that uses JsonML diffing (no JSX) with minimal hard syntax: https://github.com/danristea/DOMatic/tree/master
Also, re-rendering can be as granular as passing only the (previously instantiated) controllers associated with the components that require redrawing.
Perhaps something similar?

@drom
Copy link

drom commented May 13, 2017

I use JsonML a lot in my flow. Anybody know good react-to-jsonml custom renderier?

@paranoidjk
Copy link

@drom
Copy link

drom commented May 13, 2017

@paranoidjk I need exactly the opposite tool. That would take React VirtualDOM structure and render it into JsonML like object or object proxy. Just custom implementation of ReactDOM.render.

@gaearon
Copy link
Collaborator

gaearon commented Oct 2, 2017

Seems like changing this would not solve any practical problems we’re experiencing, but would introduce new ones.

@gaearon gaearon closed this as completed Oct 2, 2017
@streamich
Copy link

streamich commented Feb 9, 2018

Seems like changing this would not solve any practical problems we’re experiencing, but would introduce new ones.

I would actually solve 2 problems. It would remove two levels of indirection when creating AST.

  1. JSX -> React.createElement()
  2. React.createElement() -> React.ReactElement

If JSON-ML was supported natively you would not need to have those two steps, and would allow people who prefer JS over XML, to write templates directly in JS:

const Button = () => ['button', {}, 'Click me!'];

This also eliminates the React.createElement() hidden dependency, which you don't see when using JSX. If eliminated, it allows to create cross-framework templates, which are not specific to React or other v-DOM library.

Also, if template is "simple", i.e. not using complex props, like ['button', {}, 'Click me!'] it can be serialized for sending over wire or saved together with other JSON data like translations. For example, this allows to add "markup" into you translations stored in JSON.

And, one of the biggest benefits would be that JSON-ML is standartized, so any tool that works with JSON-ML could pick it up and work with it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests