-
Notifications
You must be signed in to change notification settings - Fork 1.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
Use controlled state with value #3028
Use controlled state with value #3028
Conversation
This change turns `<NumberInput>` to "controlled mode", if `value` prop exists. Such "controlled mode" switch is behind `useControlledStateWithEventListener` feature flag for now to given it's a breaking change. Also this change introduces `readOnly` prop to `<NumberInput>` with corresponding style. Refs carbon-design-system#2489.
Deploy preview for the-carbon-components ready! Built with commit 4a48fc2 https://deploy-preview-3028--the-carbon-components.netlify.com |
Deploy preview for carbon-components-react ready! Built with commit 4a48fc2 https://deploy-preview-3028--carbon-components-react.netlify.com |
Deploy preview for carbon-elements ready! Built with commit 4a48fc2 |
Why is a feature flag required for this? |
@joshblack Good question - In older version, having |
* @param {Function} propType The original prop type checker. | ||
* @returns {Function} The new prop type checker for `onChange` that makes it required if `value` exists and `readOnly` does not exist. | ||
*/ | ||
export default function requiresIfValueExists(propType) { |
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 neat, just a small grammar thing. I'd name it requiredIfValueExists
@@ -38,7 +40,11 @@ const capMax = (max, value) => | |||
class NumberInput extends Component { | |||
constructor(props) { | |||
super(props); | |||
let value = props.value; | |||
let value = | |||
useControlledStateWithValue || props.value === undefined |
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.
Important: The defaultValue
should be completely ignored if a value prop is passed in. It's only purpose is to supply an initial state for an otherwise uncontrolled component. With your current implementation, the defaultValue
will override a value
prop if both were passed in.
A few more things:
- I think we can clean up this this logic with the
capMin
helper - We should refactor to avoid relying on undefined checks and avoid setting variables to undefined.
- This refactoring means we can set a proper defaultProp for the
devaultValue
(0). - We should also add a
controlledMode
instance field to consolidates the flag checks. In my opinion, an instance field is more appropriate than state since we never intend on changing the property during the life cycle of the component.
constructor(props) {
super(props);
const { value: valueProp, defaultValue, min } = props;
this.controlledMode =
useControlledStateWithValue && valueProp !== undefined;
const value = this.controlledMode ? valueProp : defaultValue;
this.state = { value: capMin(min, value) };
}
// Add to defaultProps
// static defaultProps = {
// defaultValue: 0
// };
I have a sandbox with a demo of the constructor
@@ -254,7 +281,11 @@ class NumberInput extends Component { | |||
min, | |||
step, | |||
onChange: this.handleChange, | |||
value: this.state.value, | |||
value: | |||
useControlledStateWithValue && value !== undefined |
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 an example of a spot where we'd use the instance field.
const result = this.props.value !== undefined; | ||
if (lastIsControlled !== undefined && lastIsControlled !== result) { | ||
warning( | ||
'A component is changing an uncontrolled `NumberInput` to be controlled. ' + |
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 fact that we're trying to emulate 2 react errors feels like a code smell to me. How many errors, edge cases are we missing? This feels like a strong indication we're going the wrong direction with how we're attempting to manage the state of this component. We'd need to replicate this error in each component using this pattern for each piece of managed state?
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.
In the must have
functionality of my issue regarding this topic one of the two points was "The component should never be both controlled and uncontrolled. When a derived state value is also updated by setState calls, there isn't a single source of truth for the data.
I was a fan of the hybrid approach when it emulated separate presentational and stateful components. This implementation feels really different than the initial goal of separating controlled and uncontrolled components. This hybrid approach attempts to support controlled and uncontrolled throughout the lifecycle of the component and I fear could introduce a lot of complexity in trying to sync the states between these two disparate sources of state changes.
While we can warn developers, we should also be wary of how this will scale to components with multiple forms of potentially controlled state. Managing errors like this within components would be a lot of maintenance overhead.
Here is an example implementation that persists controlled/uncontrolled mode of the component so that react can properly generate the warnings for us: |
8d7923e
to
5e4f7f6
Compare
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.
Thanks @vpicone for your review, as we discussed offline, I made the following updates:
- Determine the component should be controlled/uncontrolled at creation time and stuck to it for the lifetime of the component instance (Will rectify this decision whenever we find application would have a problem with this)
- Added more documentation of what the new feature flag does, including the changed argument list of the event handlers
- Updated a comment in
gDSFP()
that it'll be no-op when the feature flag is turned on
Also as discussed offline, let me know if you anticipate any more documentation. Thanks!
* * _With_ this feature flag, the event handler has `onChange(event, { value, direction })` where: | ||
* * `event` is the (React) raw event | ||
* * `value` is the new value | ||
* * `direction` tells you the button you hit is up button or down button |
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 direction
seems relevant only for NumberInput, perhaps we'd rather document any "extra" properties on a per component basis.
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.
Makes sense - Moved explanation of such extra properties to <NumberInput>
.
@@ -17,4 +17,20 @@ | |||
* const MyComponent = props => (<div {...props}>{aFeatureFlag ? 'foo' : 'bar'}</div>); | |||
*/ | |||
|
|||
/* Currently no feature flag is in use, but keeping this file as a placeholder */ | |||
/** | |||
* Uses `value` prop to turn several components to controlled mode, notablly `<NumberInput>`. |
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.
With this flag, certain components will be created in either a controlled or controlled mode based on the existence of a value prop. The following components will have the significance of their props slightly altered as outlined below.
Components: <NumberInput>
value
→ when provided, enables controlled mode. For the rest of the component's lifecycle, it will be controlled by this prop as it's single source of truth.
defaultValue
→ Optional starting value, used for for uncontrolled mode only (no value
prop). The value
prop takes precedence over defaultValue
.
onChange
→ optional event handler. However, if value
is provided and a handler is not, we'll throw a warning indicating the component is now read-only
readOnly
→ silences the above warning, acknowledging the read-only state of the component
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.
Thanks @vpicone - Put it in the code.
Co-Authored-By: Vince Picone <vpicone@gmail.com>
Co-Authored-By: Vince Picone <vpicone@gmail.com>
Co-Authored-By: Vince Picone <vpicone@gmail.com>
Co-Authored-By: Vince Picone <vpicone@gmail.com>
Co-Authored-By: Vince Picone <vpicone@gmail.com>
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.
Thanks for all your work/discussion on this. Let me know if you want me to help adding the logic to other components!
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 change turns
<NumberInput>
to "controlled mode", ifvalue
prop exists. Such "controlled mode" switch is behinduseControlledStateWithEventListener
feature flag for now to given it's a breaking change.Also this change introduces
readOnly
prop to<NumberInput>
with corresponding style.Refs #2489.
Changelog
New
<NumberInput>
to "controlled mode", ifvalue
prop exists. Such "controlled mode" switch is behinduseControlledStateWithEventListener
feature flag for now to given it's a breaking change.readOnly
prop to<NumberInput>
with corresponding style.Changed
0
default value logic moved out fromdefaultProps
for the sake ofdefaultValue
support.onChange
signature to include the new value to support controlled mode. This is behinduseControlledStateWithEventListener
feature flag for now to given it's a breaking change.onChange
prop type checking logic to make it required whenvalue
exists andreadOnly
doesn't.Testing / Reviewing
Testing should make sure number input is not broken.