Randomness in React Props
I’m working on a side-project that involves these cute little animated critters:
While they’re all pretty similar from one another, they do vary in a few ways:
- Shape (rectangle, pentagon)
- Walk speed
I wanted the ability to control these props when needed, but also to have randomized values used by default, so that I could easily generate a crowd like this without worrying about having to come up with values for them.
In my mind, my ideal setup was something like this:
This didn’t work, because
defaultProps doesn’t work like that: the expression is evaluated once, when the component is first seen, and the same randomized value is used for every instance of the component. In other words, I’d get a single randomized set of values, and 10 identical People with those values.
My next idea was to move the logic to the
constructor function. When rendering a new Person, props would be copied to state, and blank values would be randomized:
I’m simply appending properties to the instance here because, in my application, I know these values are static; they never change once the component is created, so I don’t have to worry about updating them on re-render. If I did, I’d have to store these values in
this.state, and then handle updates with
This works! I have a beautifully diverse group of People pacing around.
But, I’m not super happy with it.
It feels like there are two discrete chunks of logic here:
- The data-munging and randomization to manage the props,
- Actually rendering the little person, and animating them around
I like when React components only have a single responsibility. This bugger’s doing too many things!
The simplest solution involves just creating a thin wrapper, called
Thin Wrappers wooo!
Thin wrappers are such a nice pattern. They allow you to “cut the fat” out of your presentational components, so that they can focus on their core concern, without a bunch of important-but-unrelated business getting in the way.
I used this pattern in React Flip Move, an NPM package that handles animating list shuffles automatically. The
FlipMove component was already complex enough without all the data-munging needed for its props, so I extracted a
PropConverter HOC that tackled all of this for me, so that working on
FlipMove itself would be simpler.
You can also use thin wrappers to keep “leaf” components generic, and abstract specific concerns into a reusable “shell”. For example, I may have a
<Button> component as the lowest-level primitive… but there might also be a
<RoundButton> that provides a few of the Button props but allows the user to substitute most of them… and then maybe a
<RoundGoogleButton> component that’s even more specific/concrete.
Another nice side benefit of this pattern is that our
Person component is now pure.
By pure, I mean in functional-programming terms: “given the same set of inputs, a function should produce the same output”.
In our original working example, calling a
<Person> wasn’t deterministic: by design, each one would be a little different. Now that we’ve updated it, though, we know that each
<Person> with the same props will produce an identical HTML element.
Bonus: More Explicit?
In general, I’m quite happy with the solution above.
If there’s one area I find it lacking, though, it’s a lack of data transparency.
To understand what I mean, let’s look at how you use this
What props is
Person receiving? We have no idea!
It would be nice if we had a window into what
RandomPerson was actually doing, so that we’d have a clearer sense of how our
Person component works. Plus, if we ever want to pass in our own props to override the random values, we’d have to go digging through other files to figure out what they are.
Thankfully, there are several ways around this! My personal favourite is function-as-children, a variant of the Render Prop pattern.
The big change is that now we’re expecting a
children function, and we’re invoking that function with all of our values.
Here’s how we use our new component:
Is that actually better?
You may have noticed that the render-prop solution is much more verbose; you have to manually specify each prop going through. Plus, function-as-children is a contentious pattern; I love it, but many disagree.
My opinion is that the transparency is a fair price to pay for the additional code, but this is indeed a trade-off, and your priorities might be different.
In Conclusion (TL:DR;)
By moving the randomness out of the
Person component and into a thin wrapper, we keep our
Person component pure. We also reduce the number of things it has to worry about, so that our
Person only has a single responsibility.
By using function-as-children, we make it clear how data flows from the randomization process to the Person component, and the hierarchy is visible at a higher level. Your mileage may vary, though, when deciding if that added transparency is worth the cost.
Thanks for reading! Let me know what you think on Twitter.