Repository: react
Issue: RFC: Plan for custom element attributes/properties in React 19
Labels: Component: DOM,Type: Discussion

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
  * 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
  * 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
  * 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
  * 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

# Background

When React tries to pass data to a custom element it always does so using HTML attributes.

```jsx
<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:

```html
<x-foo bar="[object Object]">
```

The workaround for this is to use a `ref` to manually set the property.

```jsx
<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](https://developers.google.com/web/fundamentals/web-components/best-practices#attributes-properties) 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:**

```jsx
<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`.

```jsx
<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"](https://reactjs.org/blog/2017/09/08/dom-attributes-in-react-16.html#why-are-we-changing-this).

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](https://developers.google.com/web/fundamentals/web-components/best-practices#always-accept-primitive-data-strings-numbers-booleans-as-either-attributes--or-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](https://github.com/w3c/webcomponents/issues/654) 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](https://developers.google.com/web/fundamentals/web-components/customelements#upgrades) 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.

```jsx
<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:**

```js
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](https://github.com/facebook/react/issues/10399#issuecomment-320847065) 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](#avoids-conflict-with-future-global-attributes) 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](https://www.emberjs.com/blog/2017/10/10/glimmer-progress-report.html#toc_component-attributes).

**Glimmer example:**

```html
<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:**

```html
<!-- Vue will serialize `foo` to an attribute string, and set `squid` using a JavaScript property -->
<custom-element :foo="bar” :squid.prop=”ink”>
```

**Angular example:**

```html
<!-- 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](#option-2--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:**

```jsx
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](https://gist.github.com/treshugart/2fb509a8828adf7fee5245bfa2a54ba7) by @treshugart, author of Skate.js, and is implemented in the [val](https://github.com/skatejs/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](#option-2--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](https://github.com/facebook/react/issues/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`.

```js
// 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:

```js
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:**

```js
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:**

```jsx
<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](https://www.webcomponents.org/element/PolymerElements/paper-input/elements/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