facebook / react

The library for web and native user interfaces.
https://react.dev
MIT License
229.24k stars 46.95k forks source link

Attributes and properties for Custom Components #7249

Open edoardocavazza opened 8 years ago

edoardocavazza commented 8 years ago

Do you want to request a feature or report a bug? Feature What is the current behavior? Custom component's properties are always set as attribute. What is the expected behavior? Maybe React should watch at the static observedAttributes property for custom elements (https://w3c.github.io/webcomponents/spec/custom/) and then decide to set an attribute or an instance property. Otherwise, objects and array could be always passed as properties, in order to avoid <custom-element prop="[object Object]"></custom-element>.

robdodson commented 7 years ago

I wanted to +1 this issue, I think it would really help out with Custom Element interop. I don't know if React needs to watch observedAttributes, instead it could do something like Preact and check for the property on the node. Or if that's too much work just default to setting properties all the time.

Any custom element built with Polymer (and I believe SkateJS) will have property setters auto-generated so you can rely on them being there for those elements. And we're encouraging developers to rely on properties as their source of truth when building custom elements.

cc @blasten @sebmarkbage @treshugart

sebmarkbage commented 7 years ago

I'm leaning towards a properties-if-available model too.

if (propName in obj) {
  obj[propName] = value;
} else {
  obj.setAttribute(propName, value);
}
treshugart commented 7 years ago

Properties-if-available is what we've done in Skate with our Incremental DOM wrapper FWIW. However, I've recently been messing around with an alternative approach, similar to the proposed events in #7249.

That repo is only a playground for me at the moment, but I really like how you have full control over what is set as properties, attributes and events by having special attributes and events props. I definitely think that's worth considering as there's no error prone internal checks that need to be done. For example, the proposed approach here doesn't work with the custom element polyfill because it's possible that it wouldn't have been upgraded yet (thus have upgraded props) by the time it does the propName in obj check.

treshugart commented 7 years ago

@robdodson

Any custom element built with Polymer (and I believe SkateJS) will have property setters auto-generated so you can rely on them being there for those elements.

Yup, we have first class props.

blasten commented 7 years ago

There's a possibility of a raise condition between upgrading a custom element and running DOM reconciliation. For example:

  1. The DOM is initially server-side rendered and the app runs on a browser that doesn't have native CE. React reconciles the DOM, but because the polyfill upgrades CEs in the next micro task, React might set an attribute or a property depending on the order of those calls. If set as an attribute and the value is an object, then the CE will end up with prop="[Object object]" or prop="1,2,3,4".

  2. Similar issue, but this time the CE upgrades asynchronously. If React were to always set the values as properties, then a CE constructor could check if a property is defined in the instance. e.g.: If it's an instance prop, remove it and call the setter in the prototype instead. Not great, but better than [Object object].

Any thoughts?

justinfagnani commented 7 years ago

@blasten is correct. If an element is not-upgraded when React sets props, then attributes would be set, and once it's upgraded the element wouldn't have access to the props like it would with properties.

treshugart commented 7 years ago

+1 for what @blasten said. After having messed around with the method I linked to in my last comment, I can say it's a very robust approach. I'm not sure there is a fool proof method other than giving the consumer full control. A positive side effect is that there's less logic necessary to decide what to do.

treshugart commented 7 years ago

As a proof of concept, for this (and #7901) I've created a gist. It is a function that wraps any hyperscript style vdom function and enables full control over props, attributes and events (including custom events) for any vdom implementation that supports ref. It even allows one to pass a custom element constructor as the component name. Details of the implementation are in the readme of the gist.

blasten commented 7 years ago

Thanks for putting that proposal together @treshugart. I think it would be great if all frameworks supported this contract:

React, Preact:

render() {
  return 
  <div ref="scroller" style={{ overflowY: 'auto' }}>
    <custom-element props={{ 
      onSomething: () => {},
      ariaLabel: 'Tap',
      scroller: () => this.ref.scroller
    }}></custom-element>
  </div>;
}
class CustomElement extends HTMLElement {
  set props(props) {
    let prevProps = this._props;
    // check prevProps.onSomething !== props.onSomething 
    // check prevProps.ariaLabel !== props.ariaLabel 
    this._props = props;
    debounce(this._render);
  }

  get props() {
    return Object.assign({}, this._props);
  }
}

The CE props setter can add event listeners or set attributes when necessary and call it's equivalent render function. The CE can also support setting those props as attributes as long as it knows how to deserialize them. (Mostly for declarative HTML documents) For example:

Declarative HTML

<div id="scrollingElement" style="overflow-y: auto;">
  <custom-element on-something="foo()" aria-label="tap" scroller="scrollingElement">
  </custom-element>
</div>

What I like about this setup is:

  1. Simple.
  2. A single setter, which drastically reduces the boilerplate for CE authors.
  3. Can supports async upgrades because the CE constructor can do something like this:
    if (this.hasOwnProperty('props')) {
    let userProps = self.props;
    delete this.props;
    this.props = userProps;
    }
  4. No expensive deserialization.
  5. No race conditions. Properties are the source of truth.

I'm curious about your guys thoughts. cc @addyosmani @robdodson @developit @justinfagnani

robdodson commented 7 years ago

A possible proposal is for CE authors to always use the properties pattern that @blasten has shown, although I don't know if we want to encourage everyone to put everything into a single props object. Instead we might want to do something like:

upgradeProperty(prop) {
  if (this.hasOwnProperty(prop)) {
    let value = this[prop];
    delete this[prop];
    this[prop] = value;
  }
}

Then the Custom Element author could use this approach with any properties their element exposes.

Here's a jsbin that illustrates this technique. h/t to @sjmiles

I believe if CE authors use this technique then React/Preact should be able to always set properties and not worry about attributes. And this will avoid situations where someone ends up with <x-foo zerp="[object Object]">.

For events I still think it's preferable to use addEventListener as discussed in https://github.com/facebook/react/issues/7901. Maybe by adding some additional syntax or a heuristic to make this a bit nicer.

Hypnosphi commented 7 years ago

There are some cases, e. g. text input's value, where React both assigns a property AND sets the corresponding attribute. Maybe this option could be helpful here as well.

robdodson commented 7 years ago

I think the issue with setting both is that you can't assign an object or array to an attribute. You have to stringify it first. Setting both at the same time may create extra work on the element's behalf because depending on the order in which you set things, it'll likely need to process the change twice.

I'm working with a teammate to produce some custom element examples that use the pattern I previously mentioned. I'll follow up on this thread when we publish what we've come up with to see what others think.

blasten commented 7 years ago

@robdodson one property vs many properties is a personal choice, but one property (e.g. props) has several advantages over the later:

  1. namespace. I remember, polymer users tried to have properties such name or id, but they realized that those properties collide with the ones inherited from HTMLElement causing inconsistencies.

  2. The upgradeProperty check becomes O(n) if you have n properties, also the boilerplate code grows as the number of properties increases. Unless, a lib defines those properties dynamically (via Object.defineProperty) which can also slow down FPT/TTI.

  3. The getter props returns the exact current state of the element; making it easier to transition to a new state.

  4. Could have the potential to map to a prop- attribute, in the same way dataset maps to data-.

robdodson commented 7 years ago

yeah I agree there are advantages to just using props. And maybe everyone eventually goes in that direction. I didn't want to assert too specific of a pattern since all of this is still pretty unexplored territory.

First I wanted to see if we could encourage React and other libraries to always set properties, while at the same time encouraging Custom Element authors to adhere to the pattern of grabbing properties off of the instance if they've already been set. If both of those worlds start to gel, then the next step would be to explore ways to optimize it, possibly using the props pattern.

treshugart commented 7 years ago

Objectively, discussing props as a single entry point for all properties is orthogonal to this issue, and highly contentious as a perf opt. Probably best to leave this to CE libs to implement if they see fit. Its prescriptive nature places it in a higher level of abstraction than the lower level browser APIs, and doesn't fall in line with how all elements expose their API as several top level props.

I still feel the consumer (React component authors) should have full control over what they want to do. If I want to set an attribute, or add an event listener, I should be able to do so without having to workaround React only setting props or an inadequate custom element implementation.

Everything always being properties is an ideal and we live in an imperfect world.

robdodson commented 7 years ago

I still feel the consumer (React component authors) should have full control over what they want to do. If I want to set an attribute, or add an event listener, I should be able to do so without having to workaround React only setting props or an inadequate custom element implementation.

Yeah I'd be cool with this as well :)

blasten commented 7 years ago

I agree that the scope of this thread isn't discussing such patterns. I just brought it up because I found it useful, but my intention wasn't to ask the react team to implement the CE integration that way. After all, they should support what is on the spec. I was just showing a potential pattern. I could have called my property "user" instead of "props"

treshugart commented 7 years ago

I was just showing a potential pattern. I could have called my property "user" instead of "props".

👍 that's an interesting pattern in itself, and definitely something that CE authors should consider :)

haugthom commented 7 years ago

Is there any investigation on this topic? It`s really annoying that we have still problems with custom components :/

gaearon commented 7 years ago

We are not working on this but it makes sense to change the behavior to a more useful one in React 17.

However it will require somebody to do the actual research into different options, weigh their pros and cons, and come up with a comprehensive proposal for the changes they want to see.

Would you like to start working on this?

robdodson commented 7 years ago

I'm happy to help if I can. I'm not much of a React expert, but I think I have a pretty good idea of how things should work on the custom element side.

I was wondering if there's been any thought toward supporting the same model that Preact uses? If it would help I could put together a doc that outlines their approach?

iansu commented 7 years ago

I'd be happy to help implement the changes in React once a plan has been formulated.

gaearon commented 7 years ago

Yes, a doc with the proposal would help. The only concern I know about Preact model is that adding a new property to the base class in the platform would become a breaking change for React/Preact users.

robdodson commented 7 years ago

@gaearon ok I can put together a doc and ping this thread for review when it's ready

haugthom commented 7 years ago

sounds good! @robdodson thx for the doc idea.

robdodson commented 7 years ago

I think I've got most of the doc written. I'm reaching out to a couple people for feedback on the draft and should be able to share next week :)

gaearon commented 7 years ago

Sweet, thanks!

sebmarkbage commented 7 years ago

I'm getting increasingly skeptical of the properties model.

1) It's infeasible to only use properties in the current ecosystem it seems. So the other alternative is to feature test for a property and if it's not the there use the attribute. This basically prevents the DOM from adding any new properties on at least Node/Element/HTMLGenericElement. Possibly even more if sub-classing is enabled. Since if a property is added on a parent class, we'd switch to use that. Maybe we could do hasOwnProperty only but even that would be sketchy and break many use cases. https://twitter.com/sebmarkbage/status/894678380225875968

2) There's no server-rendering story into custom elements. Because we have no way of knowing how a property translates to the equivalent attribute or if they even have the same names. We'd have to render no attributes and then only add the properties on the client.

It seems to me that the two namespaces can't reliably be combined. So the only alternative is to provide two namespaces in React which is not really idiomatic.

effulgentsia commented 7 years ago

Possibly even more if sub-classing is enabled.

According to https://html.spec.whatwg.org/multipage/custom-elements.html#custom-elements-core-concepts, customized built-in elements should not add new attribute names other than data-* ones. Does this cover the sub-classing concern, or is there some other form of sub-classing that we're concerned about?

This basically prevents the DOM from adding any new properties on at least Node/Element/HTMLGenericElement

This does seem like a potential problem though. Also, at any moment in time, there may be browser differences. For example, contextmenu is a global attribute currently supported in Firefox (and for which contextMenu is automatically added as a DOM property), but not in Chrome. It's currently part of the HTML 5.1 specification, but I don't know if it will get removed from there. See https://github.com/w3c/html/issues/589. Seems quite possible for there to be other cases of global property additions into HTML5 in the future, which potentially would introduce breaking changes to custom element implementations as well as to a React strategy of basing an attribute/property decision on the property existing.

effulgentsia commented 7 years ago

It seems to me that the two namespaces can't reliably be combined. So the only alternative is to provide two namespaces in React which is not really idiomatic.

I agree that it wouldn't be idiomatic if it were specified in the JSX via any sort of subdivision of props into sub-objects. (e.g., this.props.attributes, this.props.properties, etc.).

But what about providing an API for application code to inform ReactDOM about the semantics of the custom element? E.g., ReactDOM.defineCustomElementPropertyConfig(elementName, propertyConfig) where propertyConfig could be an object similar to HTMLDOMPropertyConfig, but only for the properties defined by the custom element rather than by the platform, and still use HTMLDOMPropertyConfig itself for the properties defined by the platform.

robdodson commented 7 years ago

I posted an RFC to discuss the pros and cons of a few different options over at https://github.com/facebook/react/issues/11347

jdnichollsc commented 5 years ago

any advance of this feature? Thanks in advance 👍

jfhector commented 5 years ago

I'm keen to see this feature implemented in React. Thanks a lot for your efforts making React great.

stale[bot] commented 4 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contribution.

Hypnosphi commented 4 years ago

Sorry, I have no other choice:

+1

stale[bot] commented 4 years ago

This issue has been automatically marked as stale. If this issue is still affecting you, please leave any comment (for example, "bump"), and we'll keep it open. We are sorry that we haven't been able to prioritize it yet. If you have any additional information, please include with in your comment!

effulgentsia commented 4 years ago

bump

stale[bot] commented 4 years ago

This issue has been automatically marked as stale. If this issue is still affecting you, please leave any comment (for example, "bump"), and we'll keep it open. We are sorry that we haven't been able to prioritize it yet. If you have any new additional information, please include it with your comment!

mgol commented 4 years ago

Still valid. Is there a way to disable the stale bot for this issue? It just spams & requires others to spam as well...

yordis commented 4 years ago

@mgol 🤷 https://github.com/facebook/react/blob/61dd00db24bec6305bd72908d3617b9f2a5183da/.github/stale.yml#L7-L14

stale[bot] commented 4 years ago

This issue has been automatically marked as stale. If this issue is still affecting you, please leave any comment (for example, "bump"), and we'll keep it open. We are sorry that we haven't been able to prioritize it yet. If you have any new additional information, please include it with your comment!

silenceisgolden commented 4 years ago

I believe this should not be marked as stale.

bfsmith commented 3 years ago

Bumping before it gets marked as stale again.

cdoremus commented 3 years ago

Doing my part to foil the stale bot!

IMHO, it's a crime that React is the only major JavaScript framework that does not support Web Components

gaearon commented 3 years ago

There's an active discussion in https://github.com/facebook/react/issues/11347#issuecomment-877327383 that you're welcome to contribute to if you care about this topic.

stale[bot] commented 2 years ago

This issue has been automatically marked as stale. If this issue is still affecting you, please leave any comment (for example, "bump"), and we'll keep it open. We are sorry that we haven't been able to prioritize it yet. If you have any new additional information, please include it with your comment!

silenceisgolden commented 2 years ago

Hey bot, bump to the bump, to the bump bump bump. Maybe this will be the last bump though? 🤞🏻