Skip to content
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

Merged

Conversation

asudoh
Copy link
Contributor

@asudoh asudoh commented Jun 12, 2019

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 #2489.

Changelog

New

  • A mechanism to turn <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.
  • readOnly prop to <NumberInput> with corresponding style.

Changed

  • 0 default value logic moved out from defaultProps for the sake of defaultValue support.
  • onChange signature to include the new value to support controlled mode. This is behind useControlledStateWithEventListener feature flag for now to given it's a breaking change.
  • onChange prop type checking logic to make it required when value exists and readOnly doesn't.

Testing / Reviewing

Testing should make sure number input is not broken.

asudoh added 2 commits June 12, 2019 17:34
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.
@netlify
Copy link

netlify bot commented Jun 12, 2019

Deploy preview for the-carbon-components ready!

Built with commit 4a48fc2

https://deploy-preview-3028--the-carbon-components.netlify.com

@netlify
Copy link

netlify bot commented Jun 12, 2019

Deploy preview for carbon-components-react ready!

Built with commit 4a48fc2

https://deploy-preview-3028--carbon-components-react.netlify.com

@netlify
Copy link

netlify bot commented Jun 12, 2019

Deploy preview for carbon-elements ready!

Built with commit 4a48fc2

https://deploy-preview-3028--carbon-elements.netlify.com

@joshblack
Copy link
Contributor

Why is a feature flag required for this?

@asudoh
Copy link
Contributor Author

asudoh commented Jun 13, 2019

@joshblack Good question - In older version, having value without onChange does not make the component read-only state that is different from what the newer version does. Given that it's a breaking change and needs to be roped off until a major change comes.

* @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) {
Copy link
Contributor

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
Copy link
Contributor

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

@emyarod this behavior might be related to #3013

@@ -254,7 +281,11 @@ class NumberInput extends Component {
min,
step,
onChange: this.handleChange,
value: this.state.value,
value:
useControlledStateWithValue && value !== undefined
Copy link
Contributor

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. ' +
Copy link
Contributor

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?

Copy link
Contributor

@vpicone vpicone left a 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.

@vpicone
Copy link
Contributor

vpicone commented Jun 17, 2019

Here is an example implementation that persists controlled/uncontrolled mode of the component so that react can properly generate the warnings for us:

https://codesandbox.io/s/focused-worker-hbh7l

@asudoh asudoh force-pushed the use-controlled-state-with-value branch from 8d7923e to 5e4f7f6 Compare June 17, 2019 23:42
Copy link
Contributor Author

@asudoh asudoh left a 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!

packages/react/src/internal/FeatureFlags.js Outdated Show resolved Hide resolved
* * _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
Copy link
Contributor

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.

Copy link
Contributor Author

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>`.
Copy link
Contributor

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

Copy link
Contributor Author

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.

packages/react/src/internal/FeatureFlags.js Outdated Show resolved Hide resolved
packages/react/src/internal/FeatureFlags.js Outdated Show resolved Hide resolved
asudoh and others added 2 commits June 18, 2019 12:17
Co-Authored-By: Vince Picone <vpicone@gmail.com>
Co-Authored-By: Vince Picone <vpicone@gmail.com>
Copy link
Contributor

@vpicone vpicone left a 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!

@asudoh
Copy link
Contributor Author

asudoh commented Jun 18, 2019

Thanks @vpicone for your patience in your review and your offer! It'd be great if you can coordinate with @emyarod for the work. One thought I have is that the feature flag is not needed for components that are not broken by the new mode (e.g. <TextInput>).

Copy link
Member

@emyarod emyarod left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good to me! thanks @asudoh and @vpicone for laying the groundwork for supporting controlled/uncontrolled form inputs based on value prop

@asudoh asudoh merged commit 64c5953 into carbon-design-system:master Jun 19, 2019
@asudoh asudoh deleted the use-controlled-state-with-value branch June 19, 2019 00:29
emyarod added a commit to emyarod/carbon-components-react that referenced this pull request Aug 29, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants