-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
WIP: Feature: Implement forwardRef in wp.element.createHigherOrderComponent #7557
WIP: Feature: Implement forwardRef in wp.element.createHigherOrderComponent #7557
Conversation
Okay in this 02f163e I tried passing through the ref on the OriginalComponent before its enhanced. The test for the forwardRef prop now passes and a build still appears to work okay, but there are still some failing tests elsewhere. Looks like the tests might just be related to enzyme not working with forwardRef. So reworking those tests to use react-test-renderer may be all that's needed. |
It looks like enzyme support for forwardRef might be coming with this pull |
@nerrad, I would relay on |
I can dig dropping enzyme, but the issue here is that all the failing tests are existing tests using enzyme. So it sounds like all those tests would have to be replaced with react-test-renderer. Should that be done in a separate pull? |
@aduth I'm a bit stuck with how we want to forward Refs through the HOCs as a part of this pull and I'm not sure yet I'm on the right approach (or even IF it's a good approach). Currently with the approach I've taken in here, the leaf components would be able to check for a
|
d365030
to
b48f295
Compare
I’ve wrestled with this a bit and changed things a bit and tweaked the unit-test that demonstrate one possible use case. It looks like it’d be a bit better than what I originally had, but there’s still some problems.
I’m beginning to wonder whether its a good idea to do forwardRef here and instead just leave it to individual HOC elements to configure. Part of the problem I think is that there’s not really direct access (that I can see) to the passed in component that the “OriginalComponent” is mapped to. |
If the current approach is okay, one thing we maybe could do is have So something like this: export function createHigherOrderComponent( doForwardRef = true ) {
return ( mapComponentToEnhancedComponent, modifierName ) => {
return ( OriginalComponent ) => {
const WrappedComponent = mapComponentToEnhancedComponent(
OriginalComponent );
const EnhancedComponent = doForwardRef ?
forwardRef(
( props, ref ) => {
const forwardedRef = ref !== null ? ref : props.forwardedRef;
return <WrappedComponent
{ ...props }
forwardedRef={ forwardedRef }
/>;
}
) :
WrappedComponent;
const { displayName = OriginalComponent.name || 'Component' } = OriginalComponent;
EnhancedComponent.displayName
= `${ upperFirst( camelCase( modifierName ) ) }(${ displayName })`;
return EnhancedComponent;
};
};
}
export const pure = createHigherOrderComponent( false )( ( Wrapped ) => {
if ( Wrapped.prototype instanceof Component ) {
return class extends Wrapped {
shouldComponentUpdate( nextProps, nextState ) {
return ! isShallowEqual( nextProps, this.props ) || ! isShallowEqual( nextState, this.state );
}
};
}
return class extends Component {
shouldComponentUpdate( nextProps ) {
return ! isShallowEqual( nextProps, this.props );
}
render() {
return <Wrapped { ...this.props } />;
}
};
}, 'pure' ); The only problem with the above is its a breaking change with the signature change. So this would go back to what I mentioned earlier in the ticket about maybe introducing a new |
Admittedly, I'm not versed in javascript symbols. So I'll do a bit of reading on it as well. |
Alright so the shape of the returned value from {
$$typeof: Symbol(react.forward_ref)
render: [Function]
} So the symbol is merely used to describe the type of object it is. The important thing remains that |
If we do go with the current approach (and work out some solution for testing export const Button = ( props ) => {
return <button ref={ props.forwardedRef } />;
};
export const ButtonWithRef = forwardRef( ( props, ref ) => {
const forwardedRef = ref !== null ? ref : props.forwardedRef;
return <Button { ...props } forwardedRef= { forwardedRef } />;
} ); |
packages/element/src/test/index.js
Outdated
expect( wrapper.toJSON().children[0] ).toBe( '2' ); | ||
wrapper.update( <MyComp b /> ); // Changing the prop value should rerender | ||
expect( wrapper.toJSON().children[0] ).toBe( '3' ); | ||
wrapper.root.instance.setState( { a: 1 } ); // New state value should trigger a rerender |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is what breaks on tests because forwardRef
returns an object containing a render method. So there is no component instance and thus no state exposed.
I'm wary about using the idea I had for adding an argument to the existing
|
With 12ad2bc I finally figured out how to extract the WrappedComponent instance for asserting changing state on a Component instance wrapped by |
In ed0e374 I've added a test that demonstrates my concern with |
|
What next for this pull?A summary of some of the things uncovered in this work and what needs a decision (with some potential options). Implementing
|
Thanks for spending the time working through this @nerrad . One consideration is an extreme idea that's been proposed in passing on a few occasions: Not exposing I don't love the idea of allowing a higher-order component to opt-out of ref forwarding, since it makes it less obvious for a developer to know what will happen when they assign a |
Re not exposing component: In principle I understand that. However practically, I wonder how much benefit that will really bring. Will GB still be "compatible" with custom components written in react by third parties? Will it narrow the use-case for GB components to a WP (or @WordPress package) context? I'm wary of this being a way to make it easier to bridge future compatibility issues with React lifecycles. Would this also mean dropping the use of
I'm not sure changing
I concur and came to the same conclusion over the course of doing this ticket. |
ed0e374
to
b247787
Compare
Noting this comment from @gziolo in slack:
|
React will throw an error for html elements (leaf components) that have a `forwardedRef` prop on them because camelcase props are not allowed.
efc6c7c
to
bc25fac
Compare
Note: The snapshot regenerated does look different than the original because creatHigherOrderComponent now wraps in forwardRef. Since Shallow doesn’t render deep, this results in “Component” found in the text for displayName.
used to update snapshot
This also adds mocks for the internal child components that should be tested themselves in isolation. The snapshots are thus updated to reflect the changes with mocks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at the actual implementation in createHigherOrderComponent
, it seems like the application of ref forwarding is an enhancement atop the enhanced component. In this vein, it seems as though it could be a higher-order component of its own (withForwardedRef
). This could make it more straightforward for someone who's opting in to another higher-order component but wants to respect ref
assignment. At the same time though, it requires the conscious foresight to understand that ref
assignment is otherwise lost in a higher-order component wrapper.
Developer improvement considerations notwithstanding, I also worry about the performance impact. With these changes, each of our higher-order components becomes effectively double-wrapped, correct? Perhaps even triple wrapped, if you consider the third being the result of forwardRef
.
// components cannot have non native props with camelCase and it'll throw a | ||
// warning if present. | ||
const forwardedProps = { ...props }; | ||
delete( forwardedProps.forwardedRef ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI, delete
is an operator. While this works, as written, it could wrongly imply that we're calling a function. I'd suggest just delete obj.key;
.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/delete
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You may consider something like Lodash's _.omit
to achieve the same effect more succinctly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This demonstrates why blanket forwarding of props can be faulty vs. explicitly accepting and passing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You may consider something like Lodash's _.omit to achieve the same effect more succinctly.
Or, for performance sake, to avoid creating a clone twice (which is what's happening with the object spread in the argument destructure), we can include forwardedRef
as a destructured prop. This unfortunately requires instructing ESLint that we're intentionally not using a variable.
See also:
gutenberg/packages/components/src/popover/index.js
Lines 227 to 235 in aefe0dd
// Disable reason: We generate the `...contentProps` rest as remainder | |
// of props which aren't explicitly handled by this component. | |
/* eslint-disable no-unused-vars */ | |
position, | |
range, | |
focusOnMount, | |
getAnchorRect, | |
expandOnMobile, | |
/* eslint-enable no-unused-vars */ |
@@ -214,13 +215,18 @@ export { flowRight as compose }; | |||
* | |||
* @return {WPComponent} Component class with generated display name assigned. | |||
*/ | |||
export function createHigherOrderComponent( mapComponentToEnhancedComponent, modifierName ) { | |||
export function createHigherOrderComponent( mapComponentToEnhancedComponent, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should be consistent with arguments placement, either all on one line, or each on their own line.
@@ -224,6 +224,8 @@ const CSS_PROPERTIES_SUPPORTS_UNITLESS = new Set( [ | |||
'zoom', | |||
] ); | |||
|
|||
const forwardRefSymbol = Symbol.for( 'react.forward_ref' ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This won't work as you expect in IE11. See also #8967 .
@@ -454,6 +456,11 @@ export function renderElement( element, context = {} ) { | |||
} | |||
|
|||
return renderElement( tagName( props, context ), context ); | |||
|
|||
case 'object': |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The serializer has since been updated to handle $$typeof
, which we can add to for this purpose. See #8189.
@@ -454,6 +456,11 @@ export function renderElement( element, context = {} ) { | |||
} | |||
|
|||
return renderElement( tagName( props, context ), context ); | |||
|
|||
case 'object': | |||
if ( tagName.$$typeof && tagName.$$typeof === forwardRefSymbol ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW could this be considered a separate enhancement to automatically including forwardRef
in createHigherOrderComponent
? This seems like something we need to support in the serializer regardless. Maybe worth a separate pull request.
@@ -55,6 +57,15 @@ const POST_FORMAT_BLOCK_MAP = { | |||
video: 'core/video', | |||
}; | |||
|
|||
const isEditPropValid = ( settings ) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Two things:
- This seems like an enhancement we should support separate from
forwardRef
integration intocreateHigherOrderComponent
, and could be its own pull request. - This is definitely a piercing of the "element" abstraction. It seems like a function which should be offered by
@wordpress/element
. On glance, it appears similar to but not quite the same as what's already provided throughisValidElement
. Digging further, I found there is the officialreact-is
module which includes anisValidElementType
method, which is essentially identical in purpose to what we have here. We'd have to decide how we want to fit this into our abstraction. Honestly, I'm not sure why there's aReact.isValidElement
but not also theisValidElementType
bundled; they seem appropriate together, and with that in mind I wouldn't mind if our package exportedisValidElementType
(though leveragingreact-is
underneath).
FWIW, regardless of where this goes, the two separate enhancements identified are valuable discoveries in their own right (#7557 (comment), #7557 (comment)). |
This is a very interesting idea. It probably would have to be a wrapper on the compose itself: composeWithRef( [
withHOC1,
withHOC2,
withHOC3,
withHOC4,
] )( MyComponent ) which would be internally implemented as something like: const composeWithRef = ( HOCs) => compose( [
( OriginalComponent ) =>
React.forwardRef( ( props, ref ) => <OriginalComponent { ...props } forwardedRef={ ref } /> ),
...HOCs,
( OriginalComponent ) =>
( { forwardedRef, ...props } ) => <OriginalComponent ref={ forwardedRef } { ...props } />,
] )( MyComponent ) |
Hmm, ya a withForwardedRef component (or maybe better a variation of the The performance question is important and relevant to answering my previous question because wrapping everything with |
@aduth, @gziolo - it looks like |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not that React hooks are about to land, we probably don't have to bother with forwardRef
anymore. It also turns out that this isn't something we need to workaround in Gutenberg codebase.
Do you feel like this is something that still needs to be implemented? It definitely got stale and I'm going to add a proper label to make triaging PRs easier in the future.
In my opinion, we should either close it or refresh. The latter might be not efficient though given that React hooks should make HOCs legacy approach.
I'm 👍 for closing. |
Description
Using React.forwardRef it's possible to forward refs to children components from their parents. This pull seeks to implement this with
createHigherOrderComponent
so any components wrapped by a HOC created through that will automatically have the ref passed through the HOC to them.Currently this is a work in progress and it's not working as expected:
forwardRef(forwardedRefComponent)
but doing a build with that results in errors in the console when executing the GB editor. DoingforwardRef(forwardedRefComponent).render
appears to work well for the built files but there are failing tests so I suspect there's some issues not seen in basic user testing of the editor.I'm putting this pull up just so others can see the work so far and it might be quickly apparent what needs fixed or whether this is only feasible with some refactoring of other things.
Related issues/comments:
See summary comment regarding findings. Basically we need a decision on whether to proceed (and how to proceed) with finishing off this pull considering the findings or whether individual HOCs should be responsible for implementing
forwardRef
.How has this been tested?
Checklist:
Rollout
Here's some things needing done in this branch before its ready for merge.