Skip to content

Add create-custom-element-type RFC #15

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

Closed
wants to merge 1 commit into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
243 changes: 243 additions & 0 deletions text/0000-create-custom-element-type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
- Start Date: 2018-01-22
- RFC PR: (leave this empty)
- React Issue: (leave this empty)

# Summary

The changes in this RFC specifically address how React passes data to custom
elements and listens for their DOM events.

# Basic example

This RFC has two parts.

1. React would change its current behavior such that when it passes data to a
custom element, it uses JavaScript properties instead of calling
`setAttribute`.

For example:

```
<my-element fooBar={baz}>
```

Would become equivalent to:

```
myElement.fooBar = baz;
```

2. React would add a new API, `ReactDOM.createCustomElementType()` which would
create a wrapper React component that knows how to map properties and event
handlers back to a custom element.

# Motivation

These two changes are meant to address
[#7249](https://github.com/facebook/react/issues/7249),
[#7901](https://github.com/facebook/react/issues/7901),
[#11347](https://github.com/facebook/react/issues/11347) and a handful of other
related issues. The goal is to make it easier to integrate [custom
elements](https://developers.google.com/web/fundamentals/web-components/customelements)
in React projects.

As shown on [Custom Elements
Everywhere](https://custom-elements-everywhere.com/#react), using a custom
element in React today requires a handful of workarounds to pass complex
JavaScript data (objects, and arrays) to custom element properties or listen for
their DOM events.

# Detailed design

We're proposing React make both of these changes:

## 1. Prefer JavaScript properties on Custom Elements

By default, setting a prop on a custom element will use a JavaScript property
setter. This changes React's current behavior, which uses `setAttribute()`.

For example:

```jsx
<my-element fooBar={baz}>
```

Would become equivalent to:

```jsx
myElement.fooBar = baz;
```

When calling `ReactDOMServer.renderToString()`, props on custom elements should
be set as attributes. Camel case props should be converted to all lowercase
attributes.

Example:

```jsx
const baz = 'hello';
<my-element fooBar={baz}> // before renderToString()
'<my-element foobar="hello">' // after renderToString()
```

Note: Some custom element libraries use a heuristic where camel case properties
are converted to dash-cased attributes. E.g. `.fooBar` becomes `foo-bar=""`.
Prior experience has shown that this actually confuses a fair number of
developers. Plus there is prior art in HTML for doing all lowercase attributes,
e.g. `contenteditable`, `autocomplete`, `tabindex`. For these reasons, we think
it's easiest to convert camel cased properties to lowercase attributes, without
a delimiter, when calling `ReactDOMServer.renderToString()`.

## 2. ReactDOM.createCustomElementType()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not call it just createCustomElement?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have a strong opinion. This was the name suggested by @gaearon when we were first kicking around the idea. facebook/react#11347 (comment)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On second thought, I think it would be a little misleading to call it createCustomElement since what it returns is not actually a custom element, but instead a React component that wraps a custom element.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe createCustomElementWrapper in that case?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.createCustomElementTypeWrapper()?


Developers can use the new `createCustomElementType()` API to export a wrapper
React component which knows how to map props to the underlying custom element.
Because of the changes outlined in the first point, this wrapper would only be
necessary in situations where:

- an element only exposes an attribute API with no corresponding property.
- during SSR, a property name cannot easily be mapped back to an attribute name
by lowercasing it.
- serializing a property value to an attribute value requires a custom function,
e.g. calling `JSON.stringify()`.
- you want to be able to declaratively listen for DOM events from a custom
element (currently not supported in React,
[#7901](https://github.com/facebook/react/issues/7901)).

Example signature:

```js
ReactDOM.createCustomElementType(tagName[, configuration])
```

Example configuration:

```js
export default ReactDOM.createCustomElementType('x-foo', {
propName: {
// Attribute name to map to.
attribute: string | null,

// Serialize function to convert prop value
// to attribute string.
serialize: function | null,

// Indicates the prop is an event, and the value
// should be an event handler function.
// If the event key is used, then all of the other
// keys (property, attribute, serialize) should throw
// compile warnings if set.
event: string | null
}
propName2: {
...
}
...
});
```

Example usage:

```jsx
// XFoo.js
export default ReactDOM.createCustomElementType('x-foo', {
longName: {
attribute: 'longname'
},
someJSONdata: {
attribute: 'somejsondata',
serialize: JSON.stringify
},
onURLChanged: {
event: 'urlchanged'
}
});
```

```jsx
// App.js
import XFoo from './XFoo';
import React from 'react';
import {render} from 'react-dom';

function App() {
const name = 'Ronald McDonald';
const data = {hello: 'world'};
const handleChange = function() {
console.log('the url changed!');
}

return (
<XFoo longName={name}
Copy link

@treshugart treshugart Feb 2, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm having some ideas now that I've seen this part specifically. The 95% use-case is probably that you're going to be able to have access to the custom element constructor where you're writing the React code. The rest of the time, you may want deferred upgrades.

Part 1 - you have the custom element constructor

If you have the custom element constructor, you actually don't need to define a wrapper or any information about it. You have the properties it defines as well as a way to diff and construct it if it needs to be patched.

In this scenario, React could internally just check if it's a custom element constructor and set props if defined, or attributes otherwise. They don't need the tag name, either.

I PR'd this to Preact awhile back for reference: preactjs/preact#715.

SSR-story

This assumes you have server-side support for whatever DOM features would be utilised (JSDOM / Undom / Domino, etc). If this is not the case, then it should fallback to Part 2.

Until there's support for declarative shadow DOM (whatwg/dom#510) custom elements that have shadow DOM would render the host + light DOM as it normally would with out shadow DOM (no special APIs to mention here, just the standard childNodes stuff). This requires JavaScript on the client to initialise and rehydrate the custom elements, but this is no different that what Part 2 is offering on the client.

Due to the tight coupling this part has with the DOM, it is assumed that it would be implemented as part of ReactDOM, as opposed to React.

Part 2 - deferred upgrades (no constructor)

You do what you're proposing in this RFC.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the case of "Part 1" how would server-side rendering work?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'd have to have JSDOM support or another server side implementation support, but it would render out to host + light DOM and then rehydrate on the client. I was making the assumption this would be implemented in react-dom, if possible. I'll update.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@treshugart hm I'm confused by some of your comments.

Copy link
Author

@robdodson robdodson Feb 22, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ugh, hate the new github behavior where shift-enter submits a comment instead of inserting a newline. Continuing on from above...

In your comment you start off by talking about Part 1 and using the element constructor. But Part 1 of my proposal assumed someone was just using the tag name <x-foo>. So you're commenting on a somewhat different use case.

Copy link

@treshugart treshugart Feb 26, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@robdodson

So you're commenting on a somewhat different use case.

Sure, but what I'm saying is that most of the time you have access to the constructor; this is the happy path and I think it should be the primary consideration by this RFC. I believe itt enables better ergonomics because you can infer all your information from the constructor. Manual configuration is necessary when you don't have it, but it might be worth considering making this secondary.

There would be no attempt by the custom element to render its shadow dom content server side. I'm assuming that would all boot-up client side.

Ok, that's fair. Looking back I think this would naturally happen anyways if using react-dom and it was being run under a node-dom env. Let's not worry about this.

Copy link
Author

@robdodson robdodson Feb 27, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@treshugart Are you suggesting folks would never use their custom element tag name and would instead always use the constructor in their render functions? That's not actually something I would want to do tbh.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, our ideals here are quite divergent. I was hopeful that using a custom element in React could be the same way you'd use a React component. I almost never use custom element tag names and it's been quite liberating. Sure in HTML you do, but this isn't HTML.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'm concerned that if you're using the constructor, it may be tough to distinguish a custom element from a React component. Because they are different and may have subtle differences in their behavior, it seems like if you want something to be indistinguishable from a React component it may be better to use the ReactDOM.createCustomElementType() API to wrap the custom element in an actual React component.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that's fair. I'm 100% happy with this RFC as is and am super stoked you're doing it. I really hope this gets in.

As an aside, and in terms of ergonomics, I think it would be a nice addition to eventually add support for constructors because it clean up the API, essentially making the usage of a custom element just as simple using a React component. The fact that two components can have inconsistencies isn't a WC / React thing, it's a component author thing and I think that's an orthogonal problem.

someJSONdata={data}
onURLChanged={handleChange} />
)
}

render(<App />, document.querySelector('#app'))
```

# Drawbacks

- Changing to setting properties instead of calling `setAttribute()` on custom
elements would be an API breaking change.
- However, developers can use `ReactDOM.createCustomElementType()` to get
things back in working order.
- Or, if the attribute name matches their property name, things will "just
work".

# Alternatives

There has been an immense amount of discussion of alternatives at
[#11347](https://github.com/facebook/react/issues/11347). The current proposal
was arrived at with the React team and the community after working through about
5 different contenders. For custom element authors, switching to a
properties-first approach would be very convenient for them and unlock the
ability to easily pass objects and arrays to custom elements in JSX. For folks
who want an additional layer of security, or just prefer to look of working with
React components throughout their app, they could use the
`ReactDOM.createCustomElementType()` API.

One possible area where we could tweak things would be map camelCased properties
to dash-cased attributes, instead of going all lowercase. Because all Polymer
elements follow this heuristic (and many/most(?) custom elements are written in
Polymer), it would mean that they should all continue to "just work" with no
code changes.

# Adoption strategy

**If we implement this proposal, how will existing React developers adopt it?**

This would only affect the subset of React developers who also use custom
elements. If their custom elements expose a JS properties interface (which is
recommended in [Google's best practices
docs](https://developers.google.com/web/fundamentals/web-components/best-practices#always-accept-primitive-data-strings-numbers-booleans-as-either-attributes--or-properties))
then it may require no code changes at all. However, if they don't have
corresponding properties for their attributes, they will either need to: update
their element, or update their React app to explicitly call `setAttribute()`, or
use the proposed `ReactDOM.createCustomElementType()` API.

**Is this a breaking change?**

Yes but we estimate that the number of users affected would be small.

**Can we write a codemod?**

Someone could probably write a codemod specific to their component to update it
in all of their apps.

**Should we coordinate with other projects or libraries?**

I can't think of a reason why that would be necessary, but perhaps others have
opinions :)

# How we teach this

There are already docs on [using Web Components in
React](https://reactjs.org/docs/web-components.html), so we could update them to
explain these changes. We would also need to update the `ReactDOM` API docs to
explain the new `createCustomElementType()` API. Probably would want to link to
the Web Components docs from that API explainer.

Again, we imagine this should only affect a small subset of the React community,
those folks currently trying to integrate custom elements into their apps.