React - defaultProps vs ES6 default params when destructuring (performances issues)

33.6k views Asked by At

I just came across a question about React performances when settings default values in one of my stateless functional components.

This component had a defaultProps which defined row: false, but I didn't like it because the defaultProps is at the end of the file, which actually makes it harder to see. And thus, we aren't aware of the default property. So I moved it to the function declaration directly and assigned it using ES6 default value for parameters.

const FormField = ({
  row = false,
  ...others,
}) => {
  // logic...
};

But then we argued with a coworker about this being a good idea or not. Because doing so may seem trivial, but may also have a great impact upon performances since react is not aware of the default value.

I believe in this case, it's trivial. Because it's a boolean and not an object/array and therefore won't be seen as a different value during reconciliation.


But, let's now see a more advanced use-case:

const FormField = ({
  input: { name, value, ...inputRest },
  label = capitalize(name),
  placeholder = label,
  row = false,
  meta: { touched, error, warning },
  ...others,
}) => {
  // logic...
};

Here, I base the value of placeholder from label, which itself is based on input.name. Using ES6 destructuring with default values for parameters makes the whole thing quite easy to write/understand and it works like a charm.

But is it a good idea? And if not, then how would you do it properly?

3

There are 3 answers

9
Vadorequest On BEST ANSWER

I talked to several people on Discord #reactiflux channel and actually got the answer I was looking for.

There are basically three use-case with React components, and in some of them, destructuring params will impact performances so it is important to understand what's going on under the hood.

Stateless functional component

const MyComponent = ({ name = 'John Doe', displayName = humanize(name), address = helper.getDefaultAddress() }) => {
  return (
    <div>{displayName}</div>
  );
};

This is a stateless, functional component. There is no state, and it is functional because it is not a Class instance, but a simple function.

In this case, there is no life-cycle, you cannot have a componentWillMount or shouldComponentUpdate or constructor there. And because there is no management of the life-cycle, there is no impact on performances whatsoever. This code is perfectly valid. Some may prefer to handle the default displayName value within the function body, but in the end it doesn't really matter, it won't impact performances.

Stateless non-functional component

(Do not do this!)

class MyComponent extends React.Component {
    render() {
        const { name = 'John Doe', displayName = humanize(name), address = helper.getDefaultAddress() } = this.props;
        return (
            <div>{displayName}</div>
          );
    }
}

This is a stateless non-functional component. There is no state, but it is not "functional" since it is a class. And because it is a class, extending React.Component, it means you will have a life-cycle. You can have componentWillMount or shouldComponentUpdate or constructor there.

And, because it has a life-cycle, the way of writing this component is bad. But why?

Simply put, React offers a defaultProps attribute, to deal with default props values. And it is actually better to use it when dealing with non-functional components, because it will be called by all methods that rely on this.props.

The previous code snippet creates new local variables named name and displayName, but the default values are applied for this render method only!. If you want the default values to be applied for every method, such as the ones from the React life-cycle (shouldComponentUpdate, etc.) then you must use the defaultProps instead.

So, the previous code is actually a mistake that may lead to misunderstanding about the default value of name.

Here is how it should be written instead, to get the same behavior:

class MyComponent extends React.Component {
    render() {
        const { name, displayName = humanize(name), address } = this.props;
        return (
            <div>{displayName}</div>
          );
    }
}

MyComponent.defaultProps = {
    name: 'John Doe',
    address: helper.getDefaultAddress(),
};

This is better. Because name will always be John Doe if it wasn't defined. address default value was also dealt with, but not displayName... Why?

Well, I haven't found a way around that special use-case yet. Because the displayName should be based on the name property, which we cannot access (AFAIK) when defining defaultProps. The only way I see is to deal with it in the render method directly. Maybe there is a better way.

We don't have this issue with the address property since it's not based on the MyComponent properties but rely on something totally independant which doesn't need the props.

Stateful non-functional component

It works exactly the same as "Stateless non-functional component". Because there is still a life-cycle the behavior will be the same. The fact that there is an additional internal state in the component won't change anything.


I hope this helps to understand when using destructuring with components. I really like the functional way, it's much cleaner IMHO (+1 for simplicity).

You may prefer to always use defaultProps, whether working with functional or non-functional components, it's also valid. (+1 for consistency)

Just be aware of the life-cycle with non-functional components which "requires" the use of defaultProps. But in the end the choice is always yours ;)


Edit 10-2019: defaultProps will eventually be removed from React API at some point in the future, see https://stackoverflow.com/a/56443098/2391795 and https://github.com/reactjs/rfcs/pull/107 for the RFC.

2
Taylor Jones On

Looking at the advanced use-case you have, you're adding unnecessary properties to the component. label and placeholder are dependent on other properties being passed in and in my opinion, should not be listed as a parameter of the component itself.

If I were trying to use <FormField /> in my application and I needed to look to see what dependencies that specific component has, I would be a little bit confused as to why you're creating parameters that are based off of other parameters. I would move the label and placeholder into the function's body so it's clear they are not component dependencies but simply side effects.

As far as performance is concerned here, I'm not sure there would be a significant difference in either way. Stateless components don't really have a 'backing instance' that stateful components do, which means there isn't an in memory object keeping track of the component. I believe it's just a pure function of passing parameters in and returning the view.

On that same note.. adding the PropTypes will help with the type checking.

const FormField = ({
  input: { name, value, ...inputRest },
  row = false,
  meta: { touched, error, warning },
  ...others,
}) => {
  const label = capitalize(name),
  const placeholder = label,

  return (
    // logic
  );
};

FormField.propTypes = {
  input: PropTypes.shape({
    name: PropTypes.string.isRequired,
    value: PropTypes.string,
  }).isRequired,
  meta: PropTypes.shape({
    touched: PropTypes.bool.isRequired,
    error: PropTypes.bool.isRequired,
    warning: PropTypes.bool.isRequired,
  }).isRequired,
  row: PropTypes.bool.isRequired,
};
1
DSav On

One big difference between defaultProps and default function parameters is that the former will be checked against propTypes. The require-default-props rule of eslint-plugin-react explains it very well.

One advantage of defaultProps over custom default logic in your code is that defaultProps are resolved by React before the PropTypes typechecking happens, so typechecking will also apply to your defaultProps. The same also holds true for stateless functional components: default function parameters do not behave the same as defaultProps and thus using defaultProps is still preferred.