From 3b1e80d75967fa0e4a40dd17719d2d06c3b85e0e Mon Sep 17 00:00:00 2001 From: Daniel Friesen Date: Sat, 3 Mar 2018 23:28:22 -0800 Subject: [PATCH 01/11] Initial draft of the registered props as ref RFC --- text/0000-registered-prop-as-ref.md | 128 ++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 text/0000-registered-prop-as-ref.md diff --git a/text/0000-registered-prop-as-ref.md b/text/0000-registered-prop-as-ref.md new file mode 100644 index 00000000..e8710ef8 --- /dev/null +++ b/text/0000-registered-prop-as-ref.md @@ -0,0 +1,128 @@ +- Start Date: 2018-03-03 +- RFC PR: (leave this empty) +- React Issue: (leave this empty) + +# Summary + +Provide a way of registering a non-string prop name with handler that will act on the ref and property value. + +The intent is to allow implementation of props that can provide custom events, custom attributes, and other behaviours which are simple and act on a single prop value but require the use of a complex `ref` in order to work. + +# Basic example + +Custom events are not the only you can do with registered props as refs. However they are a good example use case that is useful and requires complex ref handler behaviour to work. +```js +// Register an ref prop that binds a my-custom-event event handler and stores it in a onMyCustomEvent variable +const onMyCustomEvent = ReactDOM.registerRefProp((ref, prevRef, value, prevValue) => { + if ( prevRef && prevValue && (ref !== prevRef || value !== prevValue) ) { + prevRef.removeEventListener('my-custom-event', prevValue); + } + + if ( ref && value && (ref !== prevRef || value !== prevValue) ) { + newRef.addEventListener('my-custom-event', prevValue); + } +}); +``` +```jsx +// Use the onMyCustomEvent variable as a prop name +const MyComponent = () => ( + { console.log(e); }} /> +); +``` + +# Motivation + +React has had long-standing issues with handling of custom attributes, custom elements, and custom events. + +Custom attributes were fixed by loosening the attribute whitelist. Custom elements behaviour is [under active discussion](https://github.com/facebook/react/issues/11347). And there is still debate on how to handle [custom events in web components](https://github.com/facebook/react/issues/7901). + +These issues bring up how complex the properties/attributes/events associated with a prop can be. A string can be either the name of an attribute or a property and it's difficult to decide which it should be (`amp` must be an attribute, but `value` must be a property). A prop name could also be a request to register an event. But which props names should be events? Should we assume every event name is lowercase and turn `onValueChanged` to `value-changed`, even though it's possible a custom event may use upper case characters like `DOMContentLoaded` and vendor events like `MozOrientation`? What about capturing events? How do you define an event listener with the new `passive` option? + +These issues will likely be solved eventually, but they all seem to suggest that at least on the web there are use cases for props on DOM elements that go beyond what React can do with simple string based key props. Currently the answer to many of these issues is "define your attributes/events imperatively using a ref prop". But ref props are too complex for simple definitions of attributes/events. They work imperatively and are normally larger than a simple declaration. As a result of this and PureComponent ref function are normally defined outside of props, moving what should be prop definitions out of render(). + +I believe this RFC is not mutually exclusive with the fixes for the custom attributes and custom events issues. We can still add custom event behaviours like `domEvents`. Even if we fix all the normal custom attributes/properties and events issues, it is still possible there may be special or advanced cases that they do not cover. Or certain ecosystems may work more optimally if they were able to register ref props instead of relying on built-in attribute and event handling syntax. + +# Detailed design + +### ReactDOM.registerRefProp((ref, prevRef, value, prevValue) => void) => symbol + +A "symbol => handler" registry of ref prop handlers should be added to ReactDOM. `registerRefProp` creates a Symbol and associates it with the handler in the registry and then returns the symbol. + +In environments that do not support `Symbol` it may not be necessary for an entire `Symbol` polyfill to be included, instead a sufficiently unique and possibly random prefix to an incrementing integer should be enough to separate the result of registerRefProp from normal string props. Including a character not valid in a JSX Attribute name such as "!"" or "|" in the prefix would also make it difficult for the prefix to match an actual prop name. + +### + +`refProp` refers to the symbol returned from `registerRefProp`. The `[refProp]={value}` is not custom syntax for this RFC but a recommendation that the JSXAttribute syntax is extended to support ES2015's computed property names. + +If `` were to expand to `React.createElement('some-element', {prop: value})` then `` would expand to `React.createElement('some-element', {[refProp]: value})` and act as it would in ES2015. + +### Handling of ref props + +React does not handle ref props itself. If a ref prop is passed to a React Component it does not behave like `ref`, instead the ref prop will be available on that component's prop such that it would be returned by `props[refProp]` given that `refProp` refers to the same symbol as used in `[refProp]={value}`. + +Ref props are handled in ReactDOM for elements that refer to dom nodes. For every prop name that is present in the registry the prop is not included in the normal dom props/attributes/events handling, instead the handler registered with `registerRefProp` is called under certain changes to the node. + +A handler takes the following arguments `ref, prevRef, value, prevValue`. It is called with the following arguments in the following cases: + +- On creation/mount of the node / when the ref prop is first added to a node: + ```js + handler(node, null, propValue, null) + ``` +- When the value of the prop changes (`nextProps[refProp] !== prevProps[refProp]`): + ```js + handler(node, node, newPropValue, prevPropValue) + ``` +- When the node is removed/unmounted / when the prop is removed from the node: + ```js + handler(null, node, null, propValue) + ``` +- I am not aware of any situation where the dom node associated with a component is replaced with another one. But if it were then the handler would be called like this: + ```js + handler(newNode, prevNode, propValue, propValue) + ``` + +This pattern provides enough information to do the following: +- When `ref && (ref !== prevRef)` "setup" actions like inserting dom elements can be run +- When `prevRef && (ref !== prevRef)` "cleanup" actions like removing elements/handlers can be run +- `value !== prevValue` can be used for simple change operations like `ref.property = value;` + +Conditions that also cover multiple actions are also possible: +- `ref && (ref !== prevRef || value !== prevValue)` covers "setup" and "value change" +- `prevRef && (ref !== prevRef || value !== prevValue)` covers "value change" and "cleanup" +- `ref && value && (ref !== prevRef || value !== prevValue)` can be used to run "setup" operations on the `ref` and `value` when the ref is added or value is changed, but not if the `value` is *falsey*. Like setting up an event handler, but not if the handler is given a *falsey* value instead of a function to temporarily disable the handler. +- `prevRef && prevValue && (ref !== prevRef || value !== prevValue)` can be used to run "cleanup" operations on the `prevRef` and `prevValue` when the ref is removed or value is changed, but not if the `prevValue` was *falsey*. Like cleaning up an event handler. + +# Drawbacks + +- Putting a registry within ReactDOM means that the same `ReactDOM` must be used to `render` as was used to call `registerRefProp`. This could break if multiple instances are present. Such as ordering errors if multiple React/ReactDOM `