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

RFC: Plan for Attributes in React 16 #10399

Closed
gaearon opened this Issue Aug 7, 2017 · 22 comments

Comments

Projects
None yet
5 participants
@gaearon
Member

gaearon commented Aug 7, 2017

Note: the final plan has changed. Refer to https://facebook.github.io/react/blog/2017/09/08/dom-attributes-in-react-16.html for details on what ends up in React 16.


This is a more formal conclusion of the discussion in #7311.
It is mostly (not yet fully) implemented by #10385.

This is meant to address #140.

I wrote this doc but it’s mostly based on discussion with @nhunzaker. I decided to write it in an attempt to formalize the behavior we want, so that if there are bugs, we can refer back to this.

Current Behavior

React only lets you use “approved” camelCase properties that look organic in JavaScript:

// No warning
<div className />           // => <div class />
<img srcSet />              // => <img srcset />
<svg enableBackground />    // => <svg enable-background />

// Warns
<div class />               // => <div />
<img srcset />              // => <img />
<svg enable-background />   // => <svg />

There are two downsides to this.

Problem: Custom Attributes

You can’t pass custom, non-standard, library-specific, or not-yet-standardized attributes:

// Warns
<input nwdirectory />      // => <input />
<div ng-app />             // => <div />
<div inert />              // => <div />

This is a very popular feature request.

Problem: Maintaining a Whitelist

We currently have to maintain a whitelist of all allowed attributes, and use it even in the production build.
By being more permissive, we can drop ReactDOM size by 7% post-min/gzip without any changes to app code.

Guiding Principles

If we change the current behavior, there’s a few existing principles we want to preserve:

  • Code should behave identically in development and production. This one is pretty obvious but it constraints what we can do with the whitelist.
  • Existing applications should keep on working. We are okay getting more permissive and passing more attributes through to the DOM, but we don’t want to change React DOM APIs at this point.
  • There should be one obviously valid way to supply a property to component. For example, allowing both class and className would be ambiguous and confusing to component authors.
  • We should maintain the spirit of JavaScript-centric API. Our users have already bought into the idea that it is more important for props to be consistent when used in JavaScript, than to match the HTML/SVG specs. We don’t want to change this now.

I think there is a compromise that lets us solve the problems above without deviating from these principles.

Proposed Behavior: Overview

We drop a large part of the whitelist, but we make the behavior less strict.
These used to be ignored due to wrong casing, but now will be passed through:

<div srcset />      // works but warns
<div classname />   // works but warns
<svg CalcMode />    // works but warns

Instead of being omitted, they will only emit a warning now.
However, we still don’t pass through attributes that differ in more than casing from React version:

<div class />            // doesn't work, warns
<div accept-charset />   // doesn't work, warns
<svg stroke-dasharray /> // doesn't work, warns

This lets us drop 7% of ReactDOM bundle size and keep most of the whitelist for development only.

Proposed Behavior: In Depth

Let’s say reactAttr is the attribute name you use in React, and domAttr is its name in HTML/SVG specs.
Our whitelist is a map from reactAttr to domAttr.

In React 15, it might look like this:

reactAttr domAttr
className class
srcSet srcset
acceptCharset accept-charset
arabicForm arabic-form
strokeDashArray stroke-dasharray
calcMode calcMode

Proposed Changes to the Whitelist

We remove any attributes where lowercase(reactAttr) === lowercase(domAttr) and don’t have special behavior. In other words, we delete any attributes that “just work” in regular HTML.

reactAttr domAttr
className class
srcSet srcset
acceptCharset accept-charset
arabicForm arabic-form
strokeDashArray stroke-dasharray
calcMode calcMode

(This is where we get 7% size savings.)
We still keep the full attribute whitelist in DEV mode for warnings.

Proposed Changes to the Algorithm

Let’s say givenAttr is the attribute in user’s JSX.
We follow these steps now:

Step 1: Check if there exists a reactAttr in the whitelist equal to the givenAttr.

If it there is a match, use the corresponding domAttr name from the whitelist and exit.
For example:

<div className />        // => <div class />
<div acceptCharset />    // => <div accept-charset />
<svg strokeDashArray />  // => <svg stroke-dasharray />

This matches behavior in 15.
If there is no match, continue.

Step 2: Check if there exists a domAttr in the whitelist that lowercase(domAttr) === lowercase(givenAttr)

We’re trying to determine if the user was using a DOM version of attribute that is sufficiently different from the one suggested by React (that is, by more than casing).

In this case, don’t render anything, warn and exit.

<div class />            // => <div /> + warning
<div accept-charset />   // => <div /> + warning
<svg stroke-dasharray /> // => <svg /> + warning

So far this matches behavior in 15.

Note that this does not catch the cases that were sufficiently similar that we excluded them from the whitelist. For example:

<div srcset />           // not in the whitelist, continue the algorithm
<div classname />        // not in the whitelist, continue the algorithm
<svg CalcMode />         // not in the whitelist, continue the algorithm

This is because we don’t keep them in the whitelist anymore.
If we hit such case, continue below.

Step 3: If the value type is valid, write givenAttr to the DOM, with a warning if it deviates from React canonical API.

This is where we deviate from 15.
If we reached this stage, we render it to the DOM anyway, which may or may not be successful:

<div srcset />           // => <div srcset /> (works) + warning
<div classname />        // => <div classname /> (not very useful) + warning
<svg CalcMode  />        // => <svg CalcMode /> (works) + warning

We only render strings and numbers.

If the value is of a different type, we skip it and warn.
For numbers, we also warn (but still render it) if the value coerced to string is 'NaN'.

Success now depends on whether DOM accepts such an attribute.

However, we will still warn if there is a reactAttr that lowercase(reactAttr) === lowercase(givenAttr).

In other words, we warn if there is a canonical React API that differs in casing, such as for all above cases.

This step also captures the new requirement. Any completely unknown attributes will happily pass through:

<input nwdirectory />      // => <input nwdirectory />
<div ng-app />             // => <div ng-app />
<div inert />              // => <div inert />

Other Considerations

  • ARIA and DATA attributes still pass through. The validation of ARIA attributes has moved to be development-only. We allow (but warn on) any unexpected ARIA attributes or attributes that seem like ARIA attributes (e.g. ariaSomething). This is a minor deviation from “pass everything through” approach but seems sensible.
  • Handling of custom element attributes has not changed.
  • Handling of special attributes (e.g. style) has not changed.
  • Note how with this approach, adding support for a new DOM attribute is still possible in a minor version as long as it’s only different in casing. People would get a warning about the new canonical name for it, but at least the old name (which they might have been using) would still work. Unlike the case where we completely disallow a second name for known attributes but allow custom attributes in general.
@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Aug 7, 2017

Member

One thing we haven’t quite worked out yet: should we pass numbers and booleans through.
Benefits of doing so:

  • We can drop more things from the whitelist.
  • It matches the intuitive behavior (based on experience with standard props which allow that).

Downsides:

  • For booleans, "false" might be interpreted as truthy by the browser in some cases.
Member

gaearon commented Aug 7, 2017

One thing we haven’t quite worked out yet: should we pass numbers and booleans through.
Benefits of doing so:

  • We can drop more things from the whitelist.
  • It matches the intuitive behavior (based on experience with standard props which allow that).

Downsides:

  • For booleans, "false" might be interpreted as truthy by the browser in some cases.
@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Aug 7, 2017

Member

We have (mostly) settled on this tradeoff:

  • We pass strings and numbers through. We don’t pass booleans (but we warn about them).
  • We coerce numbers to strings before passing them through.
  • If the value was a number, and the coerced string is 'NaN', we should warn in development (but still set it).

@sebmarkbage wants to look into a few more corner cases but this is likely the last tweak.

Member

gaearon commented Aug 7, 2017

We have (mostly) settled on this tradeoff:

  • We pass strings and numbers through. We don’t pass booleans (but we warn about them).
  • We coerce numbers to strings before passing them through.
  • If the value was a number, and the coerced string is 'NaN', we should warn in development (but still set it).

@sebmarkbage wants to look into a few more corner cases but this is likely the last tweak.

@aweary

This comment has been minimized.

Show comment
Hide comment
@aweary

aweary Aug 7, 2017

Member

We pass strings and numbers through. We don’t pass booleans (but we warn about them).

Can you clarify what you mean by not passing booleans through? What are we warning about?

Member

aweary commented Aug 7, 2017

We pass strings and numbers through. We don’t pass booleans (but we warn about them).

Can you clarify what you mean by not passing booleans through? What are we warning about?

@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Aug 7, 2017

Member
<div myCustomAttr={true} />

would not render it to the DOM and would display a warning about it not being a string or number.

The concern is that browser APIs are not consistent about what "false" means. So coercing to string might not work for future boolean attributes. If React was to pass them through before it is aware of them, it could potentially render value for ={false} that browser would interpret as truthy. And then when we actually add support for it, it would "flip" for people already relying on wrong behavior. Being a breaking change.

This doesn’t affect known boolean attributes. We will still keep them in the whitelist. So you can keep passing false to them.

Member

gaearon commented Aug 7, 2017

<div myCustomAttr={true} />

would not render it to the DOM and would display a warning about it not being a string or number.

The concern is that browser APIs are not consistent about what "false" means. So coercing to string might not work for future boolean attributes. If React was to pass them through before it is aware of them, it could potentially render value for ={false} that browser would interpret as truthy. And then when we actually add support for it, it would "flip" for people already relying on wrong behavior. Being a breaking change.

This doesn’t affect known boolean attributes. We will still keep them in the whitelist. So you can keep passing false to them.

@aweary

This comment has been minimized.

Show comment
Hide comment
@aweary

aweary Aug 7, 2017

Member

This doesn’t affect known boolean attributes. We will still keep them in the whitelist.

That's what I was confused about, thanks.

Member

aweary commented Aug 7, 2017

This doesn’t affect known boolean attributes. We will still keep them in the whitelist.

That's what I was confused about, thanks.

@sebmarkbage

This comment has been minimized.

Show comment
Hide comment
@sebmarkbage

sebmarkbage Aug 7, 2017

Member

Thoughts on special casing custom elements? If there is a property on the element with the same name, then use the property instead of the attribute?

Member

sebmarkbage commented Aug 7, 2017

Thoughts on special casing custom elements? If there is a property on the element with the same name, then use the property instead of the attribute?

@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Aug 7, 2017

Member

I’d love to see that! But let’s discuss separately? Since there is already a lot to take in here, and @nhunzaker is probably close to exhaustion from working on this 😛

Member

gaearon commented Aug 7, 2017

I’d love to see that! But let’s discuss separately? Since there is already a lot to take in here, and @nhunzaker is probably close to exhaustion from working on this 😛

@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Aug 7, 2017

Member

Although it would be nice to get changes to custom elements in 16 now that we have a chance.

Member

gaearon commented Aug 7, 2017

Although it would be nice to get changes to custom elements in 16 now that we have a chance.

@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Aug 7, 2017

Member

For custom elements, let’s follow what Preact does? If a property is defined, use a property, otherwise use attribute. But for objects/arrays always use properties.

Member

gaearon commented Aug 7, 2017

For custom elements, let’s follow what Preact does? If a property is defined, use a property, otherwise use attribute. But for objects/arrays always use properties.

@sebmarkbage

This comment has been minimized.

Show comment
Hide comment
@sebmarkbage

sebmarkbage Aug 7, 2017

Member

Even for objects/arrays, we should only use properties if one exist. But we should warn and not set if none exist.

The problem with detecting properties is that in can catch a native property on the super class (e.g. Element.prototype) which could prevent browsers from adding properties with those names later. We can't detect hasOwnProperty since many will be getter/setters.

Member

sebmarkbage commented Aug 7, 2017

Even for objects/arrays, we should only use properties if one exist. But we should warn and not set if none exist.

The problem with detecting properties is that in can catch a native property on the super class (e.g. Element.prototype) which could prevent browsers from adding properties with those names later. We can't detect hasOwnProperty since many will be getter/setters.

@aweary

This comment has been minimized.

Show comment
Hide comment
@aweary

aweary Aug 7, 2017

Member

What about objects with custom toString methods? Those should still be passed through.

Member

aweary commented Aug 7, 2017

What about objects with custom toString methods? Those should still be passed through.

@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Aug 7, 2017

Member

What about objects with custom toString methods? Those should still be passed through.

Are you talking about custom elements or regular elements (focus of this issue)?

Let’s keep this issue focused on regular elements. One doesn’t block the other, and I don’t want bikeshedding over custom elements to black landing this. :-)

Member

gaearon commented Aug 7, 2017

What about objects with custom toString methods? Those should still be passed through.

Are you talking about custom elements or regular elements (focus of this issue)?

Let’s keep this issue focused on regular elements. One doesn’t block the other, and I don’t want bikeshedding over custom elements to black landing this. :-)

@sebmarkbage

This comment has been minimized.

Show comment
Hide comment
@sebmarkbage

sebmarkbage Aug 7, 2017

Member

Both. :)

Member

sebmarkbage commented Aug 7, 2017

Both. :)

@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Aug 7, 2017

Member

I don’t think we should ever let toString() be called.

For custom elements I’d expect we pass them (as objects) if property exists, and don’t pass otherwise. If you want it to be an attribute IMO you should explicitly call toString() to express your intention.

For regular elements we don’t pass them through and always warn. Since you could be accidentally spreading something from a parent component, and toString() could be dangerous (e.g. very expensive or throws).

Member

gaearon commented Aug 7, 2017

I don’t think we should ever let toString() be called.

For custom elements I’d expect we pass them (as objects) if property exists, and don’t pass otherwise. If you want it to be an attribute IMO you should explicitly call toString() to express your intention.

For regular elements we don’t pass them through and always warn. Since you could be accidentally spreading something from a parent component, and toString() could be dangerous (e.g. very expensive or throws).

@robdodson

This comment has been minimized.

Show comment
Hide comment
@robdodson

robdodson Aug 8, 2017

The problem with detecting properties is that in can catch a native property on the super class (e.g. Element.prototype) which could prevent browsers from adding properties with those names later. We can't detect hasOwnProperty since many will be getter/setters.

I think it's OK to use the in check. If a Custom Element has a property .foo and spec authors decide to add a .foo property to HTMLElement, the Custom Element should continue to work as it originally did because it overrides the base class. So existing sites would continue to work. In the future it would be up to the Custom Element author to do a revision of their element that renames that property.

My understanding is this kind of dilemma comes up a lot with specs because there are all sorts of libraries out there which might use a particular property or function and then become super popular. So spec authors will do an audit of the web and see if their proposed name is already in use. If some other popular library "owns" that name, the spec authors will use a different one.

robdodson commented Aug 8, 2017

The problem with detecting properties is that in can catch a native property on the super class (e.g. Element.prototype) which could prevent browsers from adding properties with those names later. We can't detect hasOwnProperty since many will be getter/setters.

I think it's OK to use the in check. If a Custom Element has a property .foo and spec authors decide to add a .foo property to HTMLElement, the Custom Element should continue to work as it originally did because it overrides the base class. So existing sites would continue to work. In the future it would be up to the Custom Element author to do a revision of their element that renames that property.

My understanding is this kind of dilemma comes up a lot with specs because there are all sorts of libraries out there which might use a particular property or function and then become super popular. So spec authors will do an audit of the web and see if their proposed name is already in use. If some other popular library "owns" that name, the spec authors will use a different one.

@sebmarkbage

This comment has been minimized.

Show comment
Hide comment
@sebmarkbage

sebmarkbage Aug 8, 2017

Member

@robdodson The difference here is that attribute is the common way. So if the component uses attributes there is no property that shadow the base class. We'll pick it up from the base class and start using it instead of the attribute.

This doesn't usually happen with other specs because the common case is that things shadow.

E.g. If you do var Promise = {} in the global scope that will shadow the existing Promise.

To preserve this invariant we must only use properties and never fallback to attributes.

Member

sebmarkbage commented Aug 8, 2017

@robdodson The difference here is that attribute is the common way. So if the component uses attributes there is no property that shadow the base class. We'll pick it up from the base class and start using it instead of the attribute.

This doesn't usually happen with other specs because the common case is that things shadow.

E.g. If you do var Promise = {} in the global scope that will shadow the existing Promise.

To preserve this invariant we must only use properties and never fallback to attributes.

@robdodson

This comment has been minimized.

Show comment
Hide comment
@robdodson

robdodson Aug 8, 2017

The difference here is that attribute is the common way.

What do you mean by the common way? Not sure I follow.

So if the component uses attributes there is no property that shadow the base class.

That's true. In general I encourage folks to always create a corresponding property. I'm writing up some best practices docs that cover this.

We'll pick it up from the base class and start using it instead of the attribute.

I think I see what you're saying. The Custom Element might only work off of a [foo] attribute, which has some very specific behavior, but instead of setting the attribute you set the inherited .foo property from HTMLElement, which causes a different behavior. Am I close? :)

In the wild I haven't seen that many Custom Elements that only use attributes. FWIW, if a developer uses Polymer to create their element then it'll create corresponding properties for any attributes.

To preserve this invariant we must only use properties and never fallback to attributes.

I think that's probably fine. Personally I prefer properties for everything :) If the Custom Element author is really concerned about this there is a pattern they can use to capture properties set on unupgraded instances.

robdodson commented Aug 8, 2017

The difference here is that attribute is the common way.

What do you mean by the common way? Not sure I follow.

So if the component uses attributes there is no property that shadow the base class.

That's true. In general I encourage folks to always create a corresponding property. I'm writing up some best practices docs that cover this.

We'll pick it up from the base class and start using it instead of the attribute.

I think I see what you're saying. The Custom Element might only work off of a [foo] attribute, which has some very specific behavior, but instead of setting the attribute you set the inherited .foo property from HTMLElement, which causes a different behavior. Am I close? :)

In the wild I haven't seen that many Custom Elements that only use attributes. FWIW, if a developer uses Polymer to create their element then it'll create corresponding properties for any attributes.

To preserve this invariant we must only use properties and never fallback to attributes.

I think that's probably fine. Personally I prefer properties for everything :) If the Custom Element author is really concerned about this there is a pattern they can use to capture properties set on unupgraded instances.

@syranide

This comment has been minimized.

Show comment
Hide comment
@syranide

syranide Aug 8, 2017

Contributor

@gaearon Nice write-up! Though it feels like a middle-of-the-road approach where we inherit both the naming from convention of DOM properties AND DOM attributes, with it not being obvious to the user when they're supposed to use one or the other. Like you say (if I understood you correctly), this still means the whitelist will live on in some limited way. I think there's already a lot of confusion regarding e.g. autoplay and autoPlay.

Everything needs to be considered (incl legacy) and I can't say I know a better way forward than what you've put forth (and I've gone back and forth in the past trying to wrap my head around it 😄). But ReactDOM has two things to consider; DOM attributes and DOM properties. ReactDOM fundamentally ignores DOM properties and only supports DOM attributes today (naming convention aside), i.e. we only support things that can be rendered to HTML during SSR. However, the proposed naming convention above uses DOM property naming convention but falls back to DOM attribute naming convention for unknown attributes (non-whitelist). Essentially ReactDOM takes two different concepts (attributes and properties) and mixes them into the same namespace. Ignoring legacy this seems very confusing (and jQuery committed the "same mistake" in its early life). It seems like this just becomes even more confusing for e.g. SVG where both camelCase and hyphenated property names co-exist. Custom components adds to this. All while at its core it's a very simple problem to solve if React would not transplant the DOM property naming convention to DOM attributes and then mix the two.

So TBH I'm not entirely sure where I'm going with this, but it would be interesting to know what your high-level long-term goal is. Should ReactDOM just be a "dumb" renderer? Should it be more? How much more? (controlled inputs being implemented in ReactDOM vs userspace means we're not simply a dumb renderer, but should ReactDOM extend the same courtesy to video too?). Should we be able to largely copy-paste SVG? HTML?

Don't get be wrong, all things considered, what you're proposing causes the least friction and is probably the right way forward given the legacy, but it seems like it's replacing a whitelist with special behavior. Which doesn't seem all that much better other than from the perspective of bundle size. So is there a long-term goal towards a more stable/consistent behavior or is the idea to keep the current special-case for legacy reasons? Or do you perhaps even see this as the right long-term approach?

PS. As for let {class} = Obj, you can work around it by let {'class': className} = Obj. Annoying, but not the end of the world.

Contributor

syranide commented Aug 8, 2017

@gaearon Nice write-up! Though it feels like a middle-of-the-road approach where we inherit both the naming from convention of DOM properties AND DOM attributes, with it not being obvious to the user when they're supposed to use one or the other. Like you say (if I understood you correctly), this still means the whitelist will live on in some limited way. I think there's already a lot of confusion regarding e.g. autoplay and autoPlay.

Everything needs to be considered (incl legacy) and I can't say I know a better way forward than what you've put forth (and I've gone back and forth in the past trying to wrap my head around it 😄). But ReactDOM has two things to consider; DOM attributes and DOM properties. ReactDOM fundamentally ignores DOM properties and only supports DOM attributes today (naming convention aside), i.e. we only support things that can be rendered to HTML during SSR. However, the proposed naming convention above uses DOM property naming convention but falls back to DOM attribute naming convention for unknown attributes (non-whitelist). Essentially ReactDOM takes two different concepts (attributes and properties) and mixes them into the same namespace. Ignoring legacy this seems very confusing (and jQuery committed the "same mistake" in its early life). It seems like this just becomes even more confusing for e.g. SVG where both camelCase and hyphenated property names co-exist. Custom components adds to this. All while at its core it's a very simple problem to solve if React would not transplant the DOM property naming convention to DOM attributes and then mix the two.

So TBH I'm not entirely sure where I'm going with this, but it would be interesting to know what your high-level long-term goal is. Should ReactDOM just be a "dumb" renderer? Should it be more? How much more? (controlled inputs being implemented in ReactDOM vs userspace means we're not simply a dumb renderer, but should ReactDOM extend the same courtesy to video too?). Should we be able to largely copy-paste SVG? HTML?

Don't get be wrong, all things considered, what you're proposing causes the least friction and is probably the right way forward given the legacy, but it seems like it's replacing a whitelist with special behavior. Which doesn't seem all that much better other than from the perspective of bundle size. So is there a long-term goal towards a more stable/consistent behavior or is the idea to keep the current special-case for legacy reasons? Or do you perhaps even see this as the right long-term approach?

PS. As for let {class} = Obj, you can work around it by let {'class': className} = Obj. Annoying, but not the end of the world.

@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Aug 8, 2017

Member

We were already inconsistent anyway, both with custom elements, and with data- attributes. In my experience of talking to people, they don’t think about attributes and properties—since they spent most of their time in React land I’d argue the average developer actually knows less about DOM (and the difference) than they used to when they wrote HTML and manipulated it with DOM or jQuery APIs.

If we take that into consideration, it seems like the distinction between properties and attributes is not the one that is useful to explain the public API. Whether ReactDOM uses an attribute or a property for a particular “prop” is its implementation detail. Conceptually, the mental model I propose is that:

  • For every “prop” ReactDOM decides how to set it: with a property or an attribute. You shouldn’t have to know or think about this.
  • Unless ReactDOM “knows better”, you just get an attribute (as most people would expect).
  • React offers canonical names for known browser APIs that are awkward to use from JavaScript.

The last point is important. We’re not calling them camelCase to mirror properties specifically. We’re not suggesting it means we’re using properties whenever possible, or something like this.

We’re only offering camelCase canonical APIs because they’re just less awkward to declare, pass around, and read in JavaScript. Similar to how Xamarin would offer a C#-style APIs around underlying 1:1 iOS APIs with a different naming convention.

That’s my mental model. I agree it’s not ideal but it seems like best compromise we could find.

So TBH I'm not entirely sure where I'm going with this, but it would be interesting to know what your high-level long-term goal is.

For now, it’s to solve the above two problems (too large whitelist and no custom attributes) with minimal friction. It’s not a high-level goal, but then we don’t really have ones for ReactDOM at the moment. It works well for most cases, and we’re mostly working on the core these days. I’d argue this change brings ReactDOM closer to how the community expects it to work (and how some other libraries have done it).

Will we write applications in five years with ReactDOM? I don’t know. Maybe a higher, more intentionally designed layer like react-native-web will displace it. Maybe not. With advent of custom renderers, it might even be that somebody will fork ReactDOMLite or the opposite. I think we’re not at a point where we can choose a high-level goal for it. It’s more like we’re throwing things at the wall and will see what sticks. Haha.

Member

gaearon commented Aug 8, 2017

We were already inconsistent anyway, both with custom elements, and with data- attributes. In my experience of talking to people, they don’t think about attributes and properties—since they spent most of their time in React land I’d argue the average developer actually knows less about DOM (and the difference) than they used to when they wrote HTML and manipulated it with DOM or jQuery APIs.

If we take that into consideration, it seems like the distinction between properties and attributes is not the one that is useful to explain the public API. Whether ReactDOM uses an attribute or a property for a particular “prop” is its implementation detail. Conceptually, the mental model I propose is that:

  • For every “prop” ReactDOM decides how to set it: with a property or an attribute. You shouldn’t have to know or think about this.
  • Unless ReactDOM “knows better”, you just get an attribute (as most people would expect).
  • React offers canonical names for known browser APIs that are awkward to use from JavaScript.

The last point is important. We’re not calling them camelCase to mirror properties specifically. We’re not suggesting it means we’re using properties whenever possible, or something like this.

We’re only offering camelCase canonical APIs because they’re just less awkward to declare, pass around, and read in JavaScript. Similar to how Xamarin would offer a C#-style APIs around underlying 1:1 iOS APIs with a different naming convention.

That’s my mental model. I agree it’s not ideal but it seems like best compromise we could find.

So TBH I'm not entirely sure where I'm going with this, but it would be interesting to know what your high-level long-term goal is.

For now, it’s to solve the above two problems (too large whitelist and no custom attributes) with minimal friction. It’s not a high-level goal, but then we don’t really have ones for ReactDOM at the moment. It works well for most cases, and we’re mostly working on the core these days. I’d argue this change brings ReactDOM closer to how the community expects it to work (and how some other libraries have done it).

Will we write applications in five years with ReactDOM? I don’t know. Maybe a higher, more intentionally designed layer like react-native-web will displace it. Maybe not. With advent of custom renderers, it might even be that somebody will fork ReactDOMLite or the opposite. I think we’re not at a point where we can choose a high-level goal for it. It’s more like we’re throwing things at the wall and will see what sticks. Haha.

@gaearon gaearon referenced this issue Aug 8, 2017

Closed

React 16 Umbrella ☂️ (and 15.5) #8854

117 of 120 tasks complete
@syranide

This comment has been minimized.

Show comment
Hide comment
@syranide

syranide Aug 8, 2017

Contributor

If we take that into consideration, it seems like the distinction between properties and attributes is not the one that is useful to explain the public API. Whether ReactDOM uses an attribute or a property for a particular “prop” is its implementation detail. Conceptually, the mental model I propose is that:

For every “prop” ReactDOM decides how to set it: with a property or an attribute. You shouldn’t have to know or think about this.

The only downside is that it requires special-logic in React as opposed to just plain setAttribute(), which in and of itself adds to the mental burden, because you cannot be sure that the prop works or is named as you expect from looking at MDN. And React currently is IMHO pretty poorly documented in this regard as opposed to MDN, it's hard to know what to expect if you're not intimate familiar with React's version of the DOM.

Unless ReactDOM “knows better”, you just get an attribute (as most people would expect).

I feel that what you're saying is a nice feature for beginners, but far less so for experienced people. When you're more experienced you want to know what to expect in detail up-front, currently it's basically trial-and-error. muted for <video> can be either an attribute or a property, How do I even know which it is? Is the current behavior even correct or simply overlooked? Whereas if you mirror setAttribute() exactly then you know what will happen, because the MDN has significant details on every part of it.

With advent of custom renderers, it might even be that somebody will fork ReactDOMLite or the opposite.

That was my first though, my only concern is the compatibility between custom renderers (and obviously the overhead of bundling different ones). Even as-is I suspect it wouldn't really work considering how event handling is implemented. And it also hurts the community if it fragments (everyone using their own slightly different version of the hypothetical "ReactDOMLite").

I think we’re not at a point where we can choose a high-level goal for it. It’s more like we’re throwing things at the wall and will see what sticks. Haha.

Yeah I've gotten that feeling, and I fully understand it! 😄

Anyway, I don't mean to hold up the PR, it's an interesting discussion. Thanks for your answer!

Contributor

syranide commented Aug 8, 2017

If we take that into consideration, it seems like the distinction between properties and attributes is not the one that is useful to explain the public API. Whether ReactDOM uses an attribute or a property for a particular “prop” is its implementation detail. Conceptually, the mental model I propose is that:

For every “prop” ReactDOM decides how to set it: with a property or an attribute. You shouldn’t have to know or think about this.

The only downside is that it requires special-logic in React as opposed to just plain setAttribute(), which in and of itself adds to the mental burden, because you cannot be sure that the prop works or is named as you expect from looking at MDN. And React currently is IMHO pretty poorly documented in this regard as opposed to MDN, it's hard to know what to expect if you're not intimate familiar with React's version of the DOM.

Unless ReactDOM “knows better”, you just get an attribute (as most people would expect).

I feel that what you're saying is a nice feature for beginners, but far less so for experienced people. When you're more experienced you want to know what to expect in detail up-front, currently it's basically trial-and-error. muted for <video> can be either an attribute or a property, How do I even know which it is? Is the current behavior even correct or simply overlooked? Whereas if you mirror setAttribute() exactly then you know what will happen, because the MDN has significant details on every part of it.

With advent of custom renderers, it might even be that somebody will fork ReactDOMLite or the opposite.

That was my first though, my only concern is the compatibility between custom renderers (and obviously the overhead of bundling different ones). Even as-is I suspect it wouldn't really work considering how event handling is implemented. And it also hurts the community if it fragments (everyone using their own slightly different version of the hypothetical "ReactDOMLite").

I think we’re not at a point where we can choose a high-level goal for it. It’s more like we’re throwing things at the wall and will see what sticks. Haha.

Yeah I've gotten that feeling, and I fully understand it! 😄

Anyway, I don't mean to hold up the PR, it's an interesting discussion. Thanks for your answer!

@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Aug 8, 2017

Member

I think the discussion about whether we move closer to mirror attributes or not is orthogonal (haha, I used that word) to this proposal. It is already the case that we sometimes use the one thing, and in other cases the other. We can keep changing our stance on this in the future (and likely will) but it seems like this shouldn’t block the ability to set arbitrary attributes (for which properties don’t exist in the first place). Similarly, I agree with your point about experienced users, but it doesn’t seem to me that this proposal either makes it worse or better.

Member

gaearon commented Aug 8, 2017

I think the discussion about whether we move closer to mirror attributes or not is orthogonal (haha, I used that word) to this proposal. It is already the case that we sometimes use the one thing, and in other cases the other. We can keep changing our stance on this in the future (and likely will) but it seems like this shouldn’t block the ability to set arbitrary attributes (for which properties don’t exist in the first place). Similarly, I agree with your point about experienced users, but it doesn’t seem to me that this proposal either makes it worse or better.

flarnie added a commit to acdlite/react that referenced this issue Aug 22, 2017

WIP Add the rest of the tests for what we expect re: unknown attributes
**what is the change?:**
Adds tests for the following behavior -

- Numbers and booleans should be converted to strings, and not warn
- NaN, Symbols, functions, and objects should be converted to strings,
  and *should* warn

Going to add tests for the not-warning behavior in a follow-up.

These tests are not entirely passing - we either need to change what we
expect or change the behavior.

**why make this change?:**
Gets everyone on the same page about expected behavior, and codifies it
in a maintainable way

**test plan:**
`yarn test src/renderers/dom/shared/__tests__/ReactDOMAttribute-test.js`

**issue:**
facebook#10399

flarnie added a commit to acdlite/react that referenced this issue Aug 22, 2017

WIP Add check that we *don't* warn when handling some unknown attributes
**what is the change?:**
We are testing the behavior of unknown attributes, which has changed
since React 15.

We want to *not* warn for the following cases -
- null
- undefined
- missing
- strings
- numbers
- booleans

**why make this change?:**
We want to verify that warnings don't get fired at the wrong time.

**test plan:**
`yarn test src/renderers/dom/shared/__tests__/ReactDOMAttribute-test.js`

**issue:**
facebook#10399

flarnie added a commit to acdlite/react that referenced this issue Sep 2, 2017

WIP Add the rest of the tests for what we expect re: unknown attributes
**what is the change?:**
Adds tests for the following behavior -

- Numbers and booleans should be converted to strings, and not warn
- NaN, Symbols, functions, and objects should be converted to strings,
  and *should* warn

Going to add tests for the not-warning behavior in a follow-up.

These tests are not entirely passing - we either need to change what we
expect or change the behavior.

**why make this change?:**
Gets everyone on the same page about expected behavior, and codifies it
in a maintainable way

**test plan:**
`yarn test src/renderers/dom/shared/__tests__/ReactDOMAttribute-test.js`

**issue:**
facebook#10399

flarnie added a commit to acdlite/react that referenced this issue Sep 2, 2017

WIP Add check that we *don't* warn when handling some unknown attributes
**what is the change?:**
We are testing the behavior of unknown attributes, which has changed
since React 15.

We want to *not* warn for the following cases -
- null
- undefined
- missing
- strings
- numbers
- booleans

**why make this change?:**
We want to verify that warnings don't get fired at the wrong time.

**test plan:**
`yarn test src/renderers/dom/shared/__tests__/ReactDOMAttribute-test.js`

**issue:**
facebook#10399

flarnie added a commit to acdlite/react that referenced this issue Sep 2, 2017

Update ReactDOMAttribute test based on attribute fixture
**what is the change?:**
- booleans don't get stringified
- some warnings have changed since we originally wrote this

**why make this change?:**
The attribute behavior is finalized and now we can test it :D

I also found it handy to have a row with a truly unknown attribute, so
added "imaginaryFriend".

**test plan:**
`yarn test src/renderers/dom/shared/__tests__/ReactDOMAttribute-test.js`
and comparing the tests to the attribute table

**issue:**
facebook#10399

flarnie added a commit that referenced this issue Sep 5, 2017

Moar attribute tests (#10509)
* WIP

* WIP Add the rest of the tests for what we expect re: unknown attributes

**what is the change?:**
Adds tests for the following behavior -

- Numbers and booleans should be converted to strings, and not warn
- NaN, Symbols, functions, and objects should be converted to strings,
  and *should* warn

Going to add tests for the not-warning behavior in a follow-up.

These tests are not entirely passing - we either need to change what we
expect or change the behavior.

**why make this change?:**
Gets everyone on the same page about expected behavior, and codifies it
in a maintainable way

**test plan:**
`yarn test src/renderers/dom/shared/__tests__/ReactDOMAttribute-test.js`

**issue:**
#10399

* WIP Add check that we *don't* warn when handling some unknown attributes

**what is the change?:**
We are testing the behavior of unknown attributes, which has changed
since React 15.

We want to *not* warn for the following cases -
- null
- undefined
- missing
- strings
- numbers
- booleans

**why make this change?:**
We want to verify that warnings don't get fired at the wrong time.

**test plan:**
`yarn test src/renderers/dom/shared/__tests__/ReactDOMAttribute-test.js`

**issue:**
#10399

* ran prettier

* Symbols and functions passed to unknown attributes should remove and warn

* Abstract tests a bit to make them easier to read

* Remove Markdown from test names

I don't think we use this convention anywhere.

* Add an assertion for NaN warning message

* Update ReactDOMAttribute test based on attribute fixture

**what is the change?:**
- booleans don't get stringified
- some warnings have changed since we originally wrote this

**why make this change?:**
The attribute behavior is finalized and now we can test it :D

I also found it handy to have a row with a truly unknown attribute, so
added "imaginaryFriend".

**test plan:**
`yarn test src/renderers/dom/shared/__tests__/ReactDOMAttribute-test.js`
and comparing the tests to the attribute table

**issue:**
#10399

* remove imaginaryFriend to resolve conflict

* ran prettier

flarnie added a commit to flarnie/react that referenced this issue Sep 5, 2017

Add test for warning for camelCased unknown props
**what is the change?:**
When we render unknown props, they get converted to lower-case.

This may be unexpected for people, or break what they are expecting to
happen. Let's warn in this case and ask them to explicitly pass the
lower-cased attribute name.

**why make this change?:**
To avoid corner case buggy results for users.

**test plan:**
NOTE: ~~~It does NOT pass right now. This is a known issue.
Should we change and just expect a warning, but allow the attribute
value to be set?

`yarn run test src/renderers/dom/shared/__tests__/ReactDOMAttribute-test.js`

**issue:** facebook#10399

flarnie added a commit to flarnie/react that referenced this issue Sep 8, 2017

Moar attribute tests (#10509)
* WIP

* WIP Add the rest of the tests for what we expect re: unknown attributes

**what is the change?:**
Adds tests for the following behavior -

- Numbers and booleans should be converted to strings, and not warn
- NaN, Symbols, functions, and objects should be converted to strings,
  and *should* warn

Going to add tests for the not-warning behavior in a follow-up.

These tests are not entirely passing - we either need to change what we
expect or change the behavior.

**why make this change?:**
Gets everyone on the same page about expected behavior, and codifies it
in a maintainable way

**test plan:**
`yarn test src/renderers/dom/shared/__tests__/ReactDOMAttribute-test.js`

**issue:**
facebook#10399

* WIP Add check that we *don't* warn when handling some unknown attributes

**what is the change?:**
We are testing the behavior of unknown attributes, which has changed
since React 15.

We want to *not* warn for the following cases -
- null
- undefined
- missing
- strings
- numbers
- booleans

**why make this change?:**
We want to verify that warnings don't get fired at the wrong time.

**test plan:**
`yarn test src/renderers/dom/shared/__tests__/ReactDOMAttribute-test.js`

**issue:**
facebook#10399

* ran prettier

* Symbols and functions passed to unknown attributes should remove and warn

* Abstract tests a bit to make them easier to read

* Remove Markdown from test names

I don't think we use this convention anywhere.

* Add an assertion for NaN warning message

* Update ReactDOMAttribute test based on attribute fixture

**what is the change?:**
- booleans don't get stringified
- some warnings have changed since we originally wrote this

**why make this change?:**
The attribute behavior is finalized and now we can test it :D

I also found it handy to have a row with a truly unknown attribute, so
added "imaginaryFriend".

**test plan:**
`yarn test src/renderers/dom/shared/__tests__/ReactDOMAttribute-test.js`
and comparing the tests to the attribute table

**issue:**
facebook#10399

* remove imaginaryFriend to resolve conflict

* ran prettier
@gaearon

This comment has been minimized.

Show comment
Hide comment
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment