DEV Community

loading...
Cover image for Why I stopped spreading props on React Components

Why I stopped spreading props on React Components

aurelio profile image Aurelio Originally published at nobitagit.github.io ・5 min read

Spreading props on a React Component is a very common pattern that I picked up very easily and loved from day 1.
With time though I learnt to understand the implications of using the spread operator in different contexts and came to the conclusion that most of the time spreading props in React is best avoided. Let's see why.

I stopped spreading props... but not quite

Use spread props sparingly

First of all I want to start with a clarification. The title of this post is misleading on purpose (read: clickbaity). Props spreading has its share of good use cases and is an expressive and concise pattern that can be used effectively.

A good scenario to demonstrate how props spreading can really shine is HOCs. Let's look at this example.

const withDouble = Comp => props => (
// We could wrap any type of Component here so we just pass all props down as they are
  <Comp {...props} value={props.value * 2} />
);

const SomeInput = ({ onChange, value, disabled }) => (
  <input value={value} disabled={disabled} onChange={e => onChange(e.target.value)}/>
);

const Doubled = withDouble(SomeInput);

const MyCal = () => {
  const [val, setVal] = React.useState(0);

  return (
    <>
    <SomeInput value={val} onChange={setVal} />
    <Doubled value={val} disabled />
    </>
  );
}

You can try the above code here.

MyCal is a bare-bones, extremely limited calculator that only doubles the input we type on a field.
In this case we use withDouble as a generic wrapper that can wrap and enhance any component.
As such it must remain unaware of the props that it will forward down the tree.

Being able to spread props like so <Comp {...props} /> is really powerful because we are free to enhance any component we might have. In the example above we can see that passing 'disabled' later on will actually work exactly for that reason.

<Doubled value={val} disabled /> // this prop will be forwarded to `SomeInput` that will render a read only field

Beautiful is better than ugly.

Let's look at how SomeInput is implemented.

const SomeInput = ({ onChange, value, disabled }) => (
<input value={value} disabled={disabled} onChange={e => onChange(e.target.value)}/>
);

Now that we're in love with our new skill of spreading props, we might be tempted to do this:

const SomeInput = ({ onChange, ...props }) => (
<input {...props} onChange={e => onChange(e.target.value)}/>
);

This looks much more appealing at first glance - at least to me - and I won't deny a part of me is pleased with how succinct this looks like.

If we try that the component will work just the same. The problem we just introduced may not be apparent for now, but we have lost control over which prop our underlying input will receive. Try this and you will see that randomProp will be happily forwarded to <input /> itself.

<SomeInput onChange={alert} randomProp={2} />

Had we kept our original implementation the stray property would just have been ignored.

const SomeInput = ({ onChange, value, disabled }) => (
<input value={value} disabled={disabled} onChange={e => onChange(e.target.value)}/>
);

<SomeInput
  onChange={alert} // known props, it will be passed down
  randomProp={2} // unknown one, ignored
/>

While this might seem simplistic, problems like these become more and more common as the size and complexity of the codebase grow. Applying layers and layers of Components that just pass down props without any sort of check will make it very hard to follow where data flows, and which attributes are applied where.

In the face of ambiguity, refuse the temptation to guess.

The latest example makes the case for using a type checker, to help flag non-existing props.
Actually introducing type definitions on an existing codebase with spreads all over the place is not the most pleasing experience either.
If you're into TypeScript or Flow try writing a type def this:

const MyComp = ({
  type,
  value,
  ...rest
}: Props) => (
  const className = type === 'round' ? 'Btn--round' : 'Btn';

  return (
    <Actionable className={className} {..rest}>{value}</Actionable>
  )
)

type Props = {
  type?: 'round',
  value: React.Node
  // and??
}

Guessing value and type is quite straightforward. What about the ...rest though? How should the shape look like?
We either become sloppy and allow any, which makes me question why we're even trying to type this thing, or we have to open the 'Actionable' implementation, check how props are handled there and hope that there's not another spread there (which is highly possible) otherwise we'll have open yet another file.

Once done that I would also check all instances of 'MyComp' to make sure random or outdated props are not passed by mistake. If this sounds tedious it's because it is.

Let's compare it to this other implementation.

const MyComp = ({
  type,
  value,
  colour,
  size,
  onClick,
  onHover
}: Props) => (
const className = type === 'round' ? 'Btn--round' : 'Btn';

return (
  <Actionable
    onHover={onHover}
    onClick={onClick}
    className={className}
    colour={colour}
    size={size}>
      {value}
  </Actionable>
  )
)

While we can't be 100% sure about every single type in this list of props we can do a lot without looking any further.

type Props = {
  type?: 'round',
  value: React.Node,
  colour: string,
  size: string | number,
  onClick: () => void,
  onHover: () => void
};

While this is not perfect, it's miles better than what we have above. I would even say this next lousy effort is better than the first! At least we're listing out all the acceptable props!

type Props = {
  type?: any,
  value: any,
  colour: any,
  size: any,
  onClick: any,
  onHover: any
};

This concept might seem specific to typings, but I think it represents very well the difference in terms of cognitive effort required when reading code written in one way or the other.

By listing our props we stay away from pitfalls and ensure clarity for the readers of our code.

Explicit is better than implicit.

In conclusion, while spreading props is a powerful pattern we must be aware of its drawbacks and conscious that this technique has its place and merits, but is certainly not a silver bullet.

Having props listed out clearly helps us communicate intent and capabilities of our functions. Spreading them can be used to serve a purpose, but should never come at the cost of readability or safety.

References

With time I saw that the pitfalls of spreading props are actually documented in more than one official piece of documentation.

Spread attributes can be useful but they also make it easy to pass unnecessary props to components that don’t care about them or to pass invalid HTML attributes to the DOM. We recommend using this syntax sparingly.
React docs

Also, you might be interested to know that:

Originally published on my blog.

Discussion (4)

pic
Editor guide
Collapse
arberbr profile image
Arber Braja

I like to know all the time what im passing down to another component so expect cases of HOC, i rarely use spreading props. Ofcurse its easier but in the long run sending everything to another component is the best way to introduce issues. You will always have to check that parent component and see there what you are sending or checking ReactDevTools, console logging, etc ... you can eleminate all of this by explicitely telling at the first place what props are you going to pass down.

Collapse
aurelio profile image
Aurelio Author

Hi Arber, thanks for chiming in! Just to clarify if I'm understanding correctly, do you mean you also explicitly list all props for Higher Order Components or for everything except HOCs? Interested to see if I got your opinion!

Collapse
arberbr profile image
Arber Braja

HOC is one of the rare cases when im not explicit what i pass down and i use {...props}, almost everywhere else i like to specify what im passing where.

Collapse
wolverineks profile image
Kevin Sullivan

What about?

interface MyCompProps extends ActionableProps {
  type?: 'round',
  value: React.Node
}