-
Notifications
You must be signed in to change notification settings - Fork 46.9k
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
Do not clone key and ref getter props #6268
Conversation
Flagging for performance: This adds two call to @ericmatthys Let's make sure we add a unit test to verify the exact case you're talking about. Write a simple component that passes the props object into cloneElement and assert that it does not warn. On a higher level note, I wonder how we feel about passing the props object directly to cloneElement as a config. It means the cloned child will receive all the props of the parent, when the owner should probably be passing those props directly to the child. @ericmatthys can you elaborate (in detail) why you are using this pattern? |
Thanks @jimfb. An example of a possibly problematic component: function Outer(props) {
const {
children,
populated,
error
} = props;
if (populated) {
const child = React.Children.only(children);
return React.cloneElement(child, props);
}
if (error) {
return <Error />;
}
return <Spinner />;
} Let's assume there's a "connector" component that provides props, like // OuterConnector
render() {
return <Outer {...this.props} />;
} <OuterConnector store={store}>
<InnerFoo />
</OuterConnector>
<OuterConnector store={store}>
<InnerBar />
</OuterConnector> This lets <OuterConnector1 store={store}>
<OuterConnector2>
<InnerFoo />
</OuterConnector2>
</OuterConnector1> A workaround to the issue, which does not have very obvious intentions: const {
children,
populated,
error,
...otherProps
} = props;
if (populated) {
const child = React.Children.only(children);
return React.cloneElement(child, {
populated,
error,
...otherProps
});
} If you think that approach is dangerous or broken, it'd be good to have a warning that is relevant to the violating use case. Otherwise, I can add better tests. |
I suggest you to extract something like function getRef(config) {
if (__DEV__) {
return !config.hasOwnProperty('ref') ||
Object.getOwnPropertyDescriptor(config, 'ref').get ? null : config.ref;
} else {
return config.ref === undefined ? null : config.ref;
}
}
function getKey(config) {
if (__DEV__) {
return !config.hasOwnProperty('key') ||
Object.getOwnPropertyDescriptor(config, 'key').get ? null : '' + config.key;
} else {
return config.key === undefined ? null : '' + config.key;
}
} Then you can use them both in Would you like to give it a try? |
@gaearon Yes, I will update with an approach similar to that. |
64cc0c2
to
0a1d7ea
Compare
@gaearon Sorry for the delay. It's ready for another look now. |
@ericmatthys updated the pull request. |
This doesn't merge cleanly, can you look into why? Thanks. |
A warning was added to enforce props is a plain object. It should merge cleanly now. |
@ericmatthys updated the pull request. |
var configRef = getRef(config); | ||
var configKey = getKey(config); | ||
|
||
if (configRef != null) { | ||
// Silently steal the ref from the parent. |
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.
Any specific reason why you're changing comparisons to be looser here? They used to be true for null but are now false.
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 configRef
value is returned by getRef
and can be null
. We could strictly check null
, but an undefined
or null
check felt safer.
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.
If null
values should be allowed here, I think we'd need to check config.ref
and config.key
before calling getRef
and getKey
and then always set ref
and key
to the return.
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.
Allowing key
to be null
seems like a mistake, since it is coerced to the string "null"
.
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.
I think this approach should keep behavior consistent. Let me know what you think and I'll add a test for the null
cases.
function isValidConfigRefOrKey(config, name) {
if (__DEV__) {
return config.hasOwnProperty(name) &&
!Object.getOwnPropertyDescriptor(config, name).get;
}
return config[name] !== undefined;
}
function getConfigKey(config) {
return '' + config.key;
}
...
ReactElement.createElement = function(type, config, children) {
...
if (isValidConfigRefOrKey(config, 'ref')) {
ref = config.ref;
}
if (isValidConfigRefOrKey(config, 'key')) {
key = getConfigKey(config);
}
...
ReactElement.cloneElement = function(element, config, children) {
...
if (isValidConfigRefOrKey(config, 'ref')) {
// Silently steal the ref from the parent.
ref = config.ref;
owner = ReactCurrentOwner.current;
}
if (isValidConfigRefOrKey(config, 'key')) {
key = getConfigKey(config);
}
This look sensible to me. More tests would be welcome 👍 |
@ericmatthys updated the pull request. |
@ericmatthys Ok, I see your use case, you are wrapping Connectors and want all the props to propagate down. I think you are running into this issue because you are using connectors to pull data out of your application state, rather than pulling out the application info at the application-component level. I think a better way to structure your application would be:
It is a little more typing, but it's far more explicit. Your current method of having connectors pull data out of and do a bucket-brigade of blindly passing/forwarding props makes the code much harder to reason about because you don't know what props are expected at any given stage (ie. it runs into a lot of the same maintainability problems as |
@jimfb I definitely agree, and I'm in the process of refactoring my use of connectors and stores to support that. For now, I think I'm stuck with the more implicit method due to how connectors are working in my case. Either way, it seems good to handle ref and key from config the same when creating and cloning elements, or be very strict about it and warn. |
} | ||
|
||
if (isValidConfigRefOrKey(config, '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.
Can this indirection (passing 'ref'
instead of directly reading config.ref
) cause any performance regressions in the production path? I’m not well versed in optimizations like this but it’s a super hot code path so we better make sure we don’t regress on it in prod.
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're adding potentially 3 function calls. It could very easily be reduced to 2 since getConfigKey
isn't very necessary. The function has the same undefined condition that existed previously, but it now skips assigning null again since the value is already null. That doesn't seem like enough to make any measurable difference in performance, but I'm sure if you have any thoughts on proving that empirically.
This is why I use My use case is: let children = React.Children.map(this.props.children, child => React.cloneElement(child, this.props)); |
Any updates on this or why this hasn't been merged? This issue rears its head on any use of |
Thanks a lot for the PR, and sorry it took a long time to review! I started merging this, but then found a few existing issues in the same area, and so I rebased your commit and added a bunch of new ones on top. Your PR helped surface those bugs! I’m closing but your commit will get in as a part of #6880. |
When passing all props to
cloneElement
in a dev environment, thekey
andref
getters that are automatically set on props trigger warnings unexpectedly (e.g.key is not a prop. Trying to access it will result...
). This changesReactElement
to usepropertyIsEnumerable
to decide whether to use thekey
andref
from the config object, so the getters are ignored.