-
Notifications
You must be signed in to change notification settings - Fork 4.1k
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
breaking(AutoControlledProps): Ignore undefined #788
Conversation
|
||
// not updated | ||
wrapper | ||
.should.have.state(randomProp, randomValue) |
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.
Comment here says // not updated
, but the assertion is that the randomProp
was updated from undefined
to the randomValue
. Is this correct?
Agreed, this is something we should solve.
I actually went down this same road when first creating the ACC. I recall specifically dealing with I believe it had to do with something similar to what you've noted. The difference / hang up is in trying to "remove" a prop after it has been set once. This works for props, but suddenly doesn't for the state of an ACC since it will not pull the Possible SolutionI think we need to retain the user's ability to pass We might be able to retain both features if we shallow diff props in We could try this, feedback? |
@levithomason - The issue I see there is that there's no way for the user to "give up control" without clobbering/resetting the value on the way out. Imagine a user controlling the value and then trying to give up control: // Controlled
<Modal open /> // state.open === true
<Modal open={false} /> // state.open === false
<Modal open /> // state.open === true
// Giving up control by not passing the prop (assuming state currently === true)
<Modal /> // state.open === true
// Giving up control by passing undefined (assuming state currently === true)
<Modal open={undefined} /> // state.open === ? We can...
Option 2, which is the behavior within this PR, would be the same result as if the user did not pass the prop at all. |
IMO, we should seperate props that can accept |
@jcarbo I think we're saying the same thing, but I want to confirm. Assuming / Releasing ControlUser's should be able to assume and release control of any prop at any time. I think you should have two paths to giving up control, either remove the prop or pass an Assume this represents a sequential progression of prop values // Controlled
<Modal open />
<Modal open={false} />
// Releasing control
<Modal />
// or
<Modal open={undefined} /> Persisted StateThe second question is, when control is released, what should the persisted state be? There is some code that determines the initial value of an auto controlled prop. I think we need to run the same logic when a user releases control, except ignore defaultProps of course. This way, if you have // Controlled
<Modal open /> // this.state.open === true
// Releasing control
// (set initial values again)
<Modal /> // this.state.open === false
// or
<Modal open={undefined} /> // this.state.open === false That hunk of code in ACC does exactly that. For each auto controlled prop, it sets the initial value. If there is no actual prop value, then it defaults to the correct "not on / empty" value. Final SolutionIn order to do implement this, I think we pull the the logic for initial auto controlled prop values out from where it is into a function and also use it in
|
I believe we're in agreement on this. The current PR covers most of the discussed features. The remaining feature is to compare props on I'll add this update now. |
a7f3fed
to
e95e98e
Compare
Current coverage is 100% (diff: 100%)
|
e95e98e
to
09c6b4b
Compare
OK, I've updated this PR and added some tests. You can see the relevant branch logic here. In short, if a user removes or sets a prop I'll merge once tests pass and @jcarbo gives this a sanity check scan. Also, I don't think this is a breaking change any longer, if anything, I'd call it a bug fix. |
@jcarbo would love a set of eyes on this guy. Will probably ship today either way if you're stacked. |
// Solve the next state for autoControlledProps | ||
const newState = _.transform(autoControlledProps, (acc, prop) => { | ||
const isNextUndefined = _.isUndefined(nextProps[prop]) | ||
const propWasRemoved = _.has(this.props, prop) && isNextUndefined |
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 needs to be:
const propWasRemoved = !_.isUndefined(this.props[prop]) && isNextUndefined
If you explicitly pass undefined
then _.has(this.props, prop)
will always return true because _.has
just checks if the path exists, not that the path has a defined value. This means we'd continually reinitialize to the default value.
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.
Indeed, I've updated this and similar issues in getAutoControlledStateValue()
. I've also added several tests to ensure defaults are properly handled in both instances:
initial state
✔ uses the default prop is the regular prop is undefined
✔ uses the regular prop when a default is also defined
changing props
✔ does not return state to default props when setting props undefined
else acc[prop] = this.state[prop] | ||
}, {}) | ||
|
||
this.setState(newState) |
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.
Might be worth checking if state has actually changed so we can avoid a re-render if possible.
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 final else
branch here deferred to copying the old state to persist it. This meant new state was no longer ever empty. This was unnecessary since setting state for foo
does not remove state for bar
.
I've removed the unnecessary state copying and added a check to only set state if newState is not empty.
@levithomason - left some comments that I think are worth addressing. |
I've updated both suggestions. I also added more tests for these. Finally, in light of #864, I removed use of |
Released in |
Breaking Change
This PR changes the way components auto control their state and how they release control. Previously, when a prop was removed or set to
undefined
, the component would not resume control. Now, when an auto controlled prop is removed or set toundefined
, it will resume auto control.This applies to all auto controlled prop for all components. The Dropdown
open
prop and state is used for illustration.Before
Previously, passing
undefined
to a Component's auto controlledopen
prop would control theopen
state and prevent the Dropdown from auto controlling itsopen
state:After
Now,
undefined
props are treated as non-existent so you must explicitly pass a value if you wish you prevent the Dropdown from auto controlling theopen
prop.Fixes an issue raised in #769
@levithomason - this is somewhat of an RFC as I'm not sure if this is the way we want this to work.
AutoControlledComponent
(ACC) will control props that weren't passed by the parent, and defer to the parent-defined props if passed. However, in the case of that issue with theModal
there was no way to pass theopen
prop through to thePortal
without forcing "controlled" usage. This PR updates the ACC to control props that are either missing entirely (existing behavior) or that are passed asundefined
from the parent.This seems sane to me because it follows the convention of:
One weird thing is that if you initially set a prop to any value, and then later on set it to undefined, it won't unset the value. IMHO this could seem unexpected because doing something like
{ ...props, x: undefined }
would yield an object withx === undefined
.Again, I opened this more just to start a discussion so if you don't think this makes sense I'm fine closing this. I kinda think we should support the usage suggested in #769, which this does, so if not this we'll need to figure out a different way.