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

React 18 Enhancements: Ensure default props are initialized with serially-stable values #10777

Closed
2 tasks done
Tracked by #13623
dcwarwick opened this issue Feb 17, 2022 · 5 comments · Fixed by #14720
Closed
2 tasks done
Tracked by #13623
Assignees
Labels
package: @carbon/react @carbon/react role: dev 🤖 type: infrastructure 🤖 Issues relating to devops, tech debt, etc.
Milestone

Comments

@dcwarwick
Copy link
Contributor

dcwarwick commented Feb 17, 2022

Package

carbon-components-react

Package version

10.53.0

Description

I notice that several places where defaultProps were being used before, this has been removed and instead the default prop values are being supplied as ES6 destructuring initialisers. E.g. Button, TextInput. This is fine, of course, as this is now a preferred way of handling default prop values.

However, there is a tricky anti-pattern to watch out for. The destructuring initialisers will run EVERY TIME the render function is called, so whenever a prop is NOT supplied the default value will be inserted on EVERY CALL to the render function. This is ok if the default value is primitive, or any other serially stable value: when React checks what has changed it will consider these props to have the same values and not trigger any hooks that depend on them, including rendering. However, if the default value is something that is not serially stable, the prop will appear to React to have a new value every time the function is called, and so affected hooks and rendering will happen EVERY TIME. This is usually not harmful except that it is not optimal. Optimization is one of the motivations for switching the way that default prop values are supplied.

Some examples of values that are not serially stable:

  • []
  • () => {}

For example, TextInput now has the following two default prop values:

    onChange = () => {},
    onClick = () => {},

The onChange and onClick props, if not supplied, will be initialised to a new arrow function on every call, which React will see as a new prop value on every call, so it’ll re-render the text input on every call.

Also, Button has the following default prop value:

    size = FeatureFlags.enabled('enable-v11-release') ? 'lg' : 'default',

Now, this is serially stable, but it will check the feature flag on every invocation, which is perhaps not ideal either.

When the initialization expression is not serially stable, the initialization needs to be done outside the function parameter list. So, for example, the TextInput could instead do:

const noopFn = () => {};

// ...

  onChange = noopFn,
  onClick = noopFn,

and this will now be fine.

Similarly, Button could do:

const defaultSize = FeatureFlags.enabled('enable-v11-release') ? 'lg' : 'default',

// ...

    size = defaultSize,

and that will ensure the default value is only calculated once.

For @carbon/ibm-products, we have just applied a common pattern in which ALL of the default values are actually set up in an object, and then referenced from the parameter list. This has several benefits: not only does it help ensure we don't slip into the above anti-pattern by oversight, but it also gathers the default values together in a nice neat way, making it easy to check, for example, that none of the defaulted parameters is also marked required, and providing a place to comment unusual values, etc. This approach could be applied, for example, to TextInput by doing something like:

const defaults = {
  disabled: false,
  inline: false,
  invalid: false,
  light: false,
  onChange = () => {},
  onClick = () => {},
  size: 'md',
  type: 'text',
  warn: false,
};

// ...

const TextInput = React.forwardRef(function TextInput(
  {
    className,
    disabled = defaults.disabled,
    helperText,
    hideLabel,
    id,
    inline = defaults.inline,
    invalid = defaults.invalid,
    invalidText,
    labelText,
    light = defaults.light,
    onChange = defaults.onChange,
    onClick = defaults.onClick,
    placeholder,
    readOnly,
    size = defaults.size,
    type = defaults.type,
    warn = defaults.warn,
    warnText,
    ...rest

You can see how I've applied the above convention in this PR: https://github.com/carbon-design-system/ibm-cloud-cognitive/pull/1654/files. For example, this file Card.js might be a good example to look at: https://github.com/carbon-design-system/ibm-cloud-cognitive/blob/86b8c98c32cd6426378e66c19cf1c3359ae53b07/packages/cloud-cognitive/src/components/Card/Card.js. I don't know if this is the best approach, but it seems to me to have some advantages, so I mention it in case it is of interest. It seems to me that by having a clear place where the default values are placed will help ensure that future updates don't introduce this problem again, as it is not an obvious issue for developers to have in their mind.

In any case, the default values that currently use expressions that are not serially stable should be fixed to initialize outside the parameter list, and preferably some systematic structure to help ensure this is consistently maintained going forward would be put in place.

I hope this helps.

Code of Conduct

@dcwarwick
Copy link
Contributor Author

@sstrubberg @joshblack FYI
And the following thread on Slack is the stream-of-consciousness where I realised this could be an issue: https://ibm-casdesign.slack.com/archives/C013ZTX0N6B/p1645033906734629

@falsepopsky
Copy link
Contributor

I'm not sure of the current status of this proposal, but in addition to what @dcwarwick said earlier (I totally agree), it's worth noting that defaultProps for function components will be deprecated in the next version of React.

reactjs/rfcs#107

@tay1orjones
Copy link
Member

@falsepopsky Yeah, great point. We need to refactor off of defaultProps and use default parameters instead. We added this issue as part of our React 18 umbrella issue #11308

On the main topic of "serially stable" values - this matches the recommendation from the React team a few years back: facebook/react#16905 (comment)

I'm not sure if theres any new approaches out there for us to consider in addition to this.

@tay1orjones tay1orjones added this to the 2023 Q2 milestone Mar 22, 2023
@sstrubberg sstrubberg added type: infrastructure 🤖 Issues relating to devops, tech debt, etc. package: @carbon/react @carbon/react role: dev 🤖 and removed type: bug 🐛 severity: 4 Unrelated to a user task, has a workaround or does not need a workaround package: react carbon-components-react labels Mar 23, 2023
@tay1orjones tay1orjones changed the title Some default props are being initialised with non-serially-stable values Ensure default props are initialised with serially-stable values Mar 29, 2023
@tay1orjones tay1orjones removed this from the 2023 Q2 milestone Mar 29, 2023
@sstrubberg sstrubberg changed the title Ensure default props are initialised with serially-stable values React 18 Migration: Ensure default props are initialized with serially-stable values Apr 14, 2023
@tay1orjones tay1orjones changed the title React 18 Migration: Ensure default props are initialized with serially-stable values React 18 Enhancements: Ensure default props are initialized with serially-stable values Apr 18, 2023
@sstrubberg sstrubberg added this to the 2023 Q4 milestone Apr 26, 2023
@tay1orjones
Copy link
Member

The best way to approach this is probably going to be assigning multiple team members during a single sprint to tackle this in a mob programming session alongside #13424

@icyJoseph
Copy link

And now it seems like Function Components stop supporting defaultProps altogether.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
package: @carbon/react @carbon/react role: dev 🤖 type: infrastructure 🤖 Issues relating to devops, tech debt, etc.
Projects
Archived in project
Development

Successfully merging a pull request may close this issue.

8 participants