|
| 1 | +- Start Date: 2018-03-07 |
| 2 | +- RFC PR: (leave this empty) |
| 3 | +- React Issue: (leave this empty) |
| 4 | + |
| 5 | +# Summary |
| 6 | + |
| 7 | +Provide an API that enables [`refs`](https://reactjs.org/docs/refs-and-the-dom.html) to be forwarded to a descendant (child or grandchild component). |
| 8 | + |
| 9 | +# Motivation |
| 10 | + |
| 11 | +I recently began contributing to [`react-relay` modern](https://github.com/facebook/relay/tree/master/packages/react-relay/modern) in order to prepare it for the [upcoming React async-rendering feature](https://reactjs.org/blog/2018/03/01/sneak-peek-beyond-react-16.html). |
| 12 | + |
| 13 | +One of the first challenges I encountered was that `react-relay` depends on the legacy, unstable context API, and values passed through legacy `context` aren't accessible in the new, [static `getDerivedStateFromProps` lifecycle](https://github.com/reactjs/rfcs/blob/master/text/0006-static-lifecycle-methods.md#static-getderivedstatefrompropsnextprops-props-prevstate-state-shapestate--null). The long-term plan for `react-relay` is to use the [new and improved context API](https://github.com/reactjs/rfcs/blob/master/text/0002-new-version-of-context.md) but this can't be done without dropping support for older versions of React- (something `react-relay` may not be able to do yet). |
| 14 | + |
| 15 | +In an effort to work around this, I created a smaller wrapper component to convert the necessary `context` value to a `prop` so that it could be accessed within the static lifecycle: |
| 16 | + |
| 17 | +```js |
| 18 | +function injectLegacyRelayContext(Component) { |
| 19 | + function LegacyRelayContextConsumer(props, legacyContext) { |
| 20 | + return <Component {...props} relay={legacyContext.relay} />; |
| 21 | + } |
| 22 | + |
| 23 | + LegacyRelayContextConsumer.contextTypes = { |
| 24 | + relay: RelayPropTypes.Relay |
| 25 | + }; |
| 26 | + |
| 27 | + return LegacyRelayContextConsumer; |
| 28 | +} |
| 29 | +``` |
| 30 | + |
| 31 | +Unfortunately, this change broke several projects within Facebook that depended on refs to access the inner Relay components. |
| 32 | + |
| 33 | +As I considered this, I became convinced that the new Context API naturally lends itself to similar wrapper components, e.g.: |
| 34 | +```js |
| 35 | +const ThemeContext = React.createContext("light"); |
| 36 | + |
| 37 | +function withTheme(ThemedComponent) { |
| 38 | + return function ThemeContextInjector(props) { |
| 39 | + return ( |
| 40 | + <ThemeContext.Consumer> |
| 41 | + {value => <ThemedComponent {...props} theme={value} />} |
| 42 | + </ThemeContext.Consumer> |
| 43 | + ); |
| 44 | + }; |
| 45 | +} |
| 46 | +``` |
| 47 | + |
| 48 | +Unfortunately, within the current limitations of React, there is no (transparent) way for a |
| 49 | +a `ref` to be attached to the above `ThemedComponent`. Common workarounds typically use a special prop (e.g. `componentRef`) like so: |
| 50 | +```js |
| 51 | +const ThemeContext = React.createContext("light"); |
| 52 | + |
| 53 | +function withTheme(ThemedComponent) { |
| 54 | + return function ThemeContextInjector(props) { |
| 55 | + return ( |
| 56 | + <ThemeContext.Consumer> |
| 57 | + {value => ( |
| 58 | + <ThemedComponent {...props} ref={props.componentRef} theme={value} /> |
| 59 | + )} |
| 60 | + </ThemeContext.Consumer> |
| 61 | + ); |
| 62 | + }; |
| 63 | +} |
| 64 | +``` |
| 65 | + |
| 66 | +This convention varies from project to project, and requires users of the `ThemedComponent` to be aware of the fact that it's wrapped by a higher-order component. This detail should not be important. It should be possible to use a standard React `ref` that the `ThemeContextInjector` (in this case) could forward to its child `ThemedComponent`. |
| 67 | + |
| 68 | +The idea of `ref`-forwarding [is not new](https://github.com/facebook/react/issues/4213), but the new context and other efforts like [`create-subscription`](https://github.com/facebook/react/pull/12325) greatly increase the importance of this feature. |
| 69 | + |
| 70 | +# Basic example |
| 71 | + |
| 72 | +The Relay `context` injector component ([shown above](#motivation)) could use the ref-forwarding API proposed by this RFC as follows: |
| 73 | + |
| 74 | +```js |
| 75 | +function injectLegacyRelayContext(Component) { |
| 76 | + function LegacyRelayContextConsumer(props, legacyContext) { |
| 77 | + return ( |
| 78 | + <Component |
| 79 | + {...props} |
| 80 | + relay={legacyContext.relay} |
| 81 | + ref={props.forwardedRef} |
| 82 | + /> |
| 83 | + ); |
| 84 | + } |
| 85 | + |
| 86 | + LegacyRelayContextConsumer.contextTypes = { |
| 87 | + relay: RelayPropTypes.Relay |
| 88 | + }; |
| 89 | + |
| 90 | + // Create a special React component type that exposes the external 'ref'. |
| 91 | + // This lets us pass it along as a regular prop, |
| 92 | + // And attach it as a regular React 'ref' on the component we choose. |
| 93 | + return React.forwardRef((props, ref) => ( |
| 94 | + <LegacyRelayContextConsumer {...props} forwardedRef={ref} /> |
| 95 | + )); |
| 96 | +} |
| 97 | +``` |
| 98 | + |
| 99 | +The theme context wrapper could as well: |
| 100 | + |
| 101 | +```js |
| 102 | +const ThemeContext = React.createContext("light"); |
| 103 | + |
| 104 | +function withTheme(ThemedComponent) { |
| 105 | + function ThemeContextInjector(props) { |
| 106 | + return ( |
| 107 | + <ThemeContext.Consumer> |
| 108 | + {value => ( |
| 109 | + <ThemedComponent {...props} ref={props.forwardedRef} theme={value} /> |
| 110 | + )} |
| 111 | + </ThemeContext.Consumer> |
| 112 | + ); |
| 113 | + } |
| 114 | + |
| 115 | + // Forward refs through to the inner, "themed" component: |
| 116 | + return React.forwardRef((props, ref) => ( |
| 117 | + <ThemeContextInjector {...props} forwardedRef={ref} /> |
| 118 | + )); |
| 119 | +} |
| 120 | +``` |
| 121 | + |
| 122 | +Here is another example [Dan mentioned on Twitter](https://twitter.com/dan_abramov/status/974008682311815169): |
| 123 | + |
| 124 | +```js |
| 125 | +// Tell React I want to be able to put a ref on LikeButton. |
| 126 | +const LikeButton = React.forwardRef((props, ref) => ( |
| 127 | + <div className="LikeButton"> |
| 128 | + {/* Forward LikeButton's ref to the button inside. */} |
| 129 | + <button ref={ref} onClick={props.onClick}> |
| 130 | + Like |
| 131 | + </button> |
| 132 | + </div> |
| 133 | +)); |
| 134 | + |
| 135 | +// I can put a ref on it as if it was a DOM node or a class! |
| 136 | +// (In this example, the ref points directly to the DOM node.) |
| 137 | +<LikeButton ref={myRef} />; |
| 138 | +``` |
| 139 | + |
| 140 | +# Detailed design |
| 141 | + |
| 142 | +Hopefully the usage of this API is clear from the [above examples](#basic-example), so in this section I'll outline a possible implementation strategy. |
| 143 | + |
| 144 | +The `forwardRef` function could return a wrapper object (similar to what the context API uses) with a `$$typeof` indicating that it's a ref-forwarding component. When React encounters this type, it can assign a new type-of-work to React (e.g. `UseRef`). The "begin" phase could then invoke the render prop argument, passing it `workInProgress.pendingProps` and `workInProgress.ref` to create the children, and then continue reconciliation. |
| 145 | + |
| 146 | +# Drawbacks |
| 147 | + |
| 148 | +This API increases the surface area of React slightly, and may complicate compatibility efforts for react-like frameworks (e.g. `preact-compat`). I believe this is worth the benefit of having a standardized, transparent way to forward refs. |
| 149 | + |
| 150 | +# Alternatives |
| 151 | + |
| 152 | +1. Add a new class method, e.g. `getPublicInstance` or `getWrappedInstance` that could be used to get the inner ref. (Some drawbacks listed [here](https://github.com/facebook/react/issues/4213#issuecomment-115019321).) |
| 153 | + |
| 154 | +2. Specify a ["high-level" flag on the component](https://github.com/facebook/react/issues/4213#issuecomment-115048260) that instructs React to forward refs past it. This approach could enable refs to be forwarded one level (to the immediate child) but would not enable forwarding to deeper child, e.g.: |
| 155 | + |
| 156 | +```js |
| 157 | +const ThemeContext = React.createContext("light"); |
| 158 | + |
| 159 | +function withTheme(ThemedComponent) { |
| 160 | + return function ThemeContextInjector(props) { |
| 161 | + return ( |
| 162 | + <ThemeContext.Consumer> |
| 163 | + {value => ( |
| 164 | + // ref belongs here |
| 165 | + <ThemedComponent {...props} ref={props.componentRef} theme={value} /> |
| 166 | + )} |
| 167 | + </ThemeContext.Consumer> |
| 168 | + ); |
| 169 | + }; |
| 170 | +} |
| 171 | +``` |
| 172 | + |
| 173 | +3. [Automatically forward refs for stateless functions components](https://github.com/facebook/react/issues/4213#issuecomment-115051991). (React currently warns if you try attaching a `ref` to a functional component, since there is no backing instance to reference.) This approach would not enable class components to forward refs, and so would not be sufficient, since wrapper components often require class lifecycles. It would also have the same child-depth limitations as the above option. |
| 174 | + |
| 175 | +# Adoption strategy |
| 176 | + |
| 177 | +This is a new feature. Since there are no backwards-compatibility concerns, adoption can be organic. |
| 178 | + |
| 179 | +One candidate that would immediately benefit from this feature would be [`create-subscription`](https://github.com/facebook/react/pull/12325). |
| 180 | + |
| 181 | +# How we teach this |
| 182 | + |
| 183 | +Add a section to the ["Refs and the DOM" documentation page](https://reactjs.org/docs/refs-and-the-dom.html) about ref-forwarding. |
| 184 | + |
| 185 | +Write a blog post about the new feature and when/why you might want to use it. Highlight examples using the new context API. |
| 186 | + |
| 187 | +# Unresolved questions |
| 188 | + |
| 189 | +None presently. |
0 commit comments