This is a more formal conclusion of the discussion in https://github.com/facebook/react/pull/7311.
It is mostly (not yet fully) implemented by https://github.com/facebook/react/pull/10385.
This is meant to address https://github.com/facebook/react/issues/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.
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.
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.
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.
If we change the current behavior, thereâs a few existing principles we want to preserve:
class
and className
would be ambiguous and confusing to component authors.I think there is a compromise that lets us solve the problems above without deviating from these principles.
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.
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
|
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.
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 />
ariaSomething
). This is a minor deviation from âpass everything throughâ approach but seems sensible.style
) has not changed.One thing we havenât quite worked out yet: should we pass numbers and booleans through.
Benefits of doing so:
Downsides:
"false"
might be interpreted as truthy by the browser in some cases.We have (mostly) settled on this tradeoff:
'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.
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?
<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.
This doesnât affect known boolean attributes. We will still keep them in the whitelist.
That's what I was confused about, thanks.
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?
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 đ
Although it would be nice to get changes to custom elements in 16 now that we have a chance.
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.
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.
What about objects with custom toString
methods? Those should still be passed through.
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. :-)
Both. :)
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).
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 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.
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.
@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 :smile:). 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.
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:
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.
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!
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.
Most helpful comment
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:
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.
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.