Description
This is meant to address #7249. The doc outlines the pros and cons of various approaches React could use to handle attributes and properties on custom elements.
TOC/Summary
- Background
- Proposals
- Option 1: Only set properties
- Pros
- Easy to understand/implement
- Avoids conflict with future global attributes
- Takes advantage of custom element "upgrade"
- Custom elements treated like any other React component
- Cons
- Possibly a breaking change
- Need ref to set attribute
- Not clear how server-side rendering would work
- Pros
- Option 2: Properties-if-available
- Pros
- Non-breaking change
- Cons
- Developers need to understand the heuristic
- Falling back to attributes may conflict with future globals
- Pros
- Option 3: Differentiate properties with a sigil
- Pros
- Non-breaking change that developers can opt-in to
- Similar to how other libraries handle attributes/properties
- The system is explicit
- Cons
- It’s new syntax
- Not clear how server-side rendering would work
- Pros
- Option 4: Add an attributes object
- Pros
- The system is explicit
- Extending syntax may also solve issues with event handling
- Cons
- It’s new syntax
- It may be a breaking change
- It may be a larger change than any of the previous proposals
- Pros
- Option 5: An API for consuming custom elements
- Pros
- The system is explicit
- Non-breaking change
- Idiomatic to React
- Cons
- Could be a lot of work for a complex component
- May bloat bundle size
- Config needs to keep pace with the component
- Pros
- Option 1: Only set properties
Background
When React tries to pass data to a custom element it always does so using HTML attributes.
<x-foo bar={baz}> // same as setAttribute('bar', baz)
Because attributes must be serialized to strings, this approach creates problems when the data being passed is an object or array. In that scenario, we end up with something like:
<x-foo bar="[object Object]">
The workaround for this is to use a ref
to manually set the property.
<x-foo ref={el => el.bar = baz}>
This workaround feels a bit unnecessary as the majority of custom elements being shipped today are written with libraries which automatically generate JavaScript properties that back all of their exposed attributes. And anyone hand-authoring a vanilla custom element is encouraged to follow this practice as well. We'd like to ideally see runtime communication with custom elements in React use JavaScript properties by default.
This doc outlines a few proposals for how React could be updated to make this happen.
Proposals
Option 1: Only set properties
Rather than try to decide if a property or attribute should be set, React could always set properties on custom elements. React would NOT check to see if the property exists on the element beforehand.
Example:
<x-foo bar={baz}>
The above code would result in React setting the .bar
property of the x-foo
element equal to the value of baz
.
For camelCased property names, React could use the same style it uses today for properties like tabIndex
.
<x-foo squidInk={pasta}> // sets .squidInk = pasta
Pros
Easy to understand/implement
This model is simple, explicit, and dovetails with React’s "JavaScript-centric API to the DOM".
Any element created with libraries like Polymer or Skate will automatically generate properties to back their exposed attributes. These elements should all "just work" with the above approach. Developers hand-authoring vanilla components are encouraged to back attributes with properties as that mirrors how modern (i.e. not oddballs like <input>
) HTML5 elements (<video>
, <audio>
, etc.) have been implemented.
Avoids conflict with future global attributes
When React sets an attribute on a custom element there’s always the risk that a future version of HTML will ship a similarly named attribute and break things. This concern was discussed with spec authors but there is no clear solution to the problem. Avoiding attributes entirely (except when a developer explicitly sets one using ref
) may sidestep this issue until the browsers come up with a better solution.
Takes advantage of custom element "upgrade"
Custom elements can be lazily upgraded on the page and some PRPL patterns rely on this technique. During the upgrade process, a custom element can access the properties passed to it by React—even if those properties were set before the definition loaded—and use them to render initial state.
Custom elements treated like any other React component
When React components pass data to one another they already use properties. This would just make custom elements behave the same way.
Cons
Possibly a breaking change
If a developer has been hand-authoring vanilla custom elements which only have an attributes API, then they will need to update their code or their app will break. The fix would be to use a ref
to set the attribute (explained below).
Need ref to set attribute
By changing the behavior so properties are preferred, it means developers will need to use a ref
in order to explicitly set an attribute on a custom element.
<custom-element ref={el => el.setAttribute('my-attr', val)} />
This is just a reversal of the current behavior where developers need a ref
in order to set a property. Since developers should rarely need to set attributes on custom elements, this seems like a reasonable trade-off.
Not clear how server-side rendering would work
It's not clear how this model would map to server-side rendering custom elements. React could assume that the properties map to similarly named attributes and attempt to set those on the server, but this is far from bulletproof and would possibly require a heuristic for things like camelCased properties -> dash-cased attributes.
Option 2: Properties-if-available
At runtime React could attempt to detect if a property is present on a custom element. If the property is present React will use it, otherwise it will fallback to setting an attribute. This is the model Preact uses to deal with custom elements.
Pseudocode implementation:
if (propName in element) {
element[propName] = value;
} else {
element.setAttribute(propName.toLowerCase(), value);
}
Possible steps:
-
If an element has a defined property, React will use it.
-
If an element has an undefined property, and React is trying to pass it primitive data (string/number/boolean), it will use an attribute.
- Alternative: Warn and don’t set.
-
If an element has an undefined property, and React is trying to pass it an object/array it will set it as a property. This is because some-attr="[object Object]” is not useful.
- Alternative: Warn and don’t set.
-
If the element is being rendered on the server, and React is trying to pass it a string/number/boolean, it will use an attribute.
-
If the element is being rendered on the server, and React is trying to pass it a object/array, it will not do anything.
Pros
Non-breaking change
It is possible to create a custom element that only uses attributes as its interface. This authoring style is NOT encouraged, but it may happen regardless. If a custom element author is relying on this behavior then this change would be non-breaking for them.
Cons
Developers need to understand the heuristic
Developers might be confused when React sets an attribute instead of a property depending on how they’ve chosen to load their element.
Falling back to attributes may conflict with future globals
Sebastian raised a concern that using in
to check for the existence of a property on a custom element might accidentally detect a property on the superclass (HTMLElement).
There are also other potential conflicts with global attributes discussed previously in this doc.
Option 3: Differentiate properties with a sigil
React could continue setting attributes on custom elements, but provide a sigil that developers could use to explicitly set properties instead. This is similar to the approach used by Glimmer.js.
Glimmer example:
<custom-img @src="corgi.jpg" @hiResSrc="corgi@2x.jpg" width="100%">
In the above example, the @ sigil indicates that src
and hiResSrc
should pass data to the custom element using properties, and width
should be serialized to an attribute string.
Because React components already pass data to one another using properties, there would be no need for them to use the sigil (although it would work if they did, it would just be redundant). Instead, it would primarily be used as an explicit instruction to pass data to a custom element using JavaScript properties.
h/t to @developit of Preact for suggesting this approach :)
Pros
Non-breaking change that developers can opt-in to
All pre-existing React + custom element apps would continue to work exactly as they have. Developers could choose if they wanted to update their code to use the new sigil style.
Similar to how other libraries handle attributes/properties
Similar to Glimmer, both Angular and Vue use modifiers to differentiate between attributes and properties.
Vue example:
<!-- Vue will serialize `foo` to an attribute string, and set `squid` using a JavaScript property -->
<custom-element :foo="bar” :squid.prop=”ink”>
Angular example:
<!-- Angular will serialize `foo` to an attribute string, and set `squid` using a JavaScript property -->
<custom-element [attr.foo]="bar” [squid]=”ink”>
The system is explicit
Developers can tell React exactly what they want instead of relying on a heuristic like the properties-if-available approach.
Cons
It’s new syntax
Developers need to be taught how to use it and it needs to be thoroughly tested to make sure it is backwards compatible.
Not clear how server-side rendering would work
Should the sigil switch to using a similarly named attribute?
Option 4: Add an attributes object
React could add additional syntax which lets authors explicitly pass data as attributes. If developers do not use this attributes object, then their data will be passed using JavaScript properties.
Example:
const bar = 'baz';
const hello = 'World';
const width = '100%';
const ReactElement = <Test
foo={bar} // uses JavaScript property
attrs={{ hello, width }} // serialized to attributes
/>;
This idea was originally proposed by @treshugart, author of Skate.js, and is implemented in the val library.
Pros
The system is explicit
Developers can tell React exactly what they want instead of relying on a heuristic like the properties-if-available approach.
Extending syntax may also solve issues with event handling
Note: This is outside the scope of this document but maybe worth mentioning :)
Issue #7901 requests that React bypass its synthetic event system when declarative event handlers are added to custom elements. Because custom element event names are arbitrary strings, it means they can be capitalized in any fashion. To bypass the synthetic event system today will also mean needing to come up with a heuristic for mapping event names from JSX to addEventListener
.
// should this listen for: 'foobar', 'FooBar', or 'fooBar'?
onFooBar={handleFooBar}
However, if the syntax is extended to allow attributes it could also be extended to allow events as well:
const bar = 'baz';
const hello = 'World';
const SquidChanged = e => console.log('yo');
const ReactElement = <Test
foo={bar}
attrs={{ hello }}
events={{ SquidChanged}} // addEventListener('SquidChanged', …)
/>;
In this model the variable name is used as the event name. No heuristic is needed.
Cons
It’s new syntax
Developers need to be taught how to use it and it needs to be thoroughly tested to make sure it is backwards compatible.
It may be a breaking change
If any components already rely on properties named attrs
or events
, it could break them.
It may be a larger change than any of the previous proposals
For React 17 it may be easier to make an incremental change (like one of the previous proposals) and position this proposal as something to take under consideration for a later, bigger refactor.
Option 5: An API for consuming custom elements
This proposal was offered by @sophiebits and @gaearon from the React team
React could create a new API for consuming custom elements that maps the element’s behavior with a configuration object.
Pseudocode example:
const XFoo = ReactDOM.createCustomElementType({
element: ‘x-foo’,
‘my-attr’: // something that tells React what to do with it
someRichDataProp: // something that tells React what to do with it
});
The above code returns a proxy component, XFoo
that knows how to pass data to a custom element depending on the configuration you provide. You would use this proxy component in your app instead of using the custom element directly.
Example usage:
<XFoo someRichDataProp={...} />
Pros
The system is explicit
Developers can tell React the exact behavior they want.
Non-breaking change
Developers can opt-in to using the object or continue using the current system.
Idiomatic to React
This change doesn’t require new JSX syntax, and feels more like other APIs in React. For example, PropTypes (even though it’s being moved into its own package) has a somewhat similar approach.
Cons
Could be a lot of work for a complex component
Polymer’s paper-input element has 37 properties, so it would produce a very large config. If developers are using a lot of custom elements in their app, that may equal a lot of configs they need to write.
May bloat bundle size
Related to the above point, each custom element class now incurs the cost of its definition + its config object size.
Note: I'm not 100% sure if this is true. Someone more familiar with the React build process could verify.
Config needs to keep pace with the component
Every time the component does a minor version revision that adds a new property, the config will need to be updated as well. That’s not difficult, but it does add maintenance. Maybe if configs are generated from source this is less of a burden, but that may mean needing to create a new tool to generate configs for each web component library.
cc @sebmarkbage @gaearon @developit @treshugart @justinfagnani