Description
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
andclassName
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.