Writing reusable React components
In this article I will share my experience and lessons learned while using React for the last couple of years. You will learn how to write reusable and extensible React components.
Read React docs
I assume, that you have basic knowledge of React. If you haven’t read React docs yet - this is where you should start.
Yeah, I know. You don’t have time to read docs right now. Or maybe you’ve started to read them, but then realized that you already know how to use React, so further reading doesn’t make a lot of sense.
OK, I understand that. I’ve been there. I know that feeling, when you can’t wait to actually start writing something.
But I encourage you to double think about that, because React docs are really great. I recommend to read all the docs, including “Advanced Guides” and “FAQ” sections. This won’t take long, but I bet you’ll learn a lot. Even if you have some experience with React.
Spread rest of props to root element
Whenever you write a component that renders something, it always has a root element. Remember to pass rest of props to that element - this will allow to modify component from outside without changing component itself.
function FancyButton(children, ...props) {
return <button {...props}>{children}</button>;
}
Now FancyButton
accepts any prop supported by root element
(HTML button
) - e.g. disabled
, type
, name
etc:
<FancyButton
onClick={() => alert("Clicked")}
disabled={isLoading}
name="submit"
>
Click me!
</FancyButton>
Same technique applies to non-HTML root elements, e.g:
function FancyIconButon(children, iconName, ...props) {
return (
<FancyButton {...props}>
<Icon name={iconName} />
{children}
</FancyButton>
);
}
Allow to modify root element styles
If component’s root element uses className
or styles
prop,
spreading rest of props to it may lead to resetting its style:
// file.css
.fancy-button {
background-color: 'orange';
}
.margin-top {
margin-top: 8px;
}
// FancyButton.jsx
function FancyButton(children, ...props) {
return (
// ⚠️ `props.className` will override "fancy-button" class
<button className="fancy-button" {...props}>
{children}
</button>
);
}
<FancyButton className="margin-top">Click me!</FancyButton>
In this case, I only want to add margin to button, but it turns out that background color is gone now.
To solve this, you should handle className
prop separately.
You can use clsx or
classnames utils to concat class names:
function FancyButton(children, className, ...props) {
return (
// ✅ "fancy-button" class is preserved
<button className={clsx("fancy-button", className)} {...props}>
{children}
</button>
);
}
It’s even easier in React Native, since style
prop can accept an array of styles:
function FancyButton(children, style, ...props) {
return (
// ✅ `FancyButton` internal styles are preserved
<TouchableOpacity style={[{ backgroundColor: "orange" }, style]}>
{children}
</TouchableOpacity>
);
}
Group props for nested components
Let’s take a FancyIconButon
for example. It takes iconName
prop and passes it to Icon
component,
so we can use different icons in different buttons:
function FancyIconButon(children, iconName, ...props) {
return (
<FancyButton {...props}>
<Icon name={iconName} />
{children}
</FancyButton>
);
}
But what if we want to slightly modify Icon
styles, and maybe pass some additional props to it?
Sure, we can add iconClassName
, iconStyles
, icon<Whatever>
props to FancyIconButton
-
it’s a good starting point.
But often it’s easier and better to group all Icon
props into one IconProps
prop, so we don’t
have to maintain each prop separately:
function FancyIconButon(children, IconProps, ...props) {
return (
<FancyButton {...props}>
<Icon {...IconProps} />
{children}
</FancyButton>
);
}
// ...
<FancyIconButon
IconProps={{
name: "accept",
className: "icon-styles",
}}
>
Click me!
</FancyIconButon>;
Use containment (aka “slots”)
Sometimes props grouping is not enough.
What if we want to pass different icon component to our FancyIconButton
from example above?
It’s not possible right now.
But with containment pattern, we can delegate full control over icon to parent component:
function FancyIconButon(children, icon, ...props) {
return (
<FancyButton {...props}>
{icon}
{children}
</FancyButton>
);
}
// ...
<FancyIconButton
icon={<FancyIcon name="accept" />}
>
Click me!
</FancyIconButon>
Use render props
Render props pattern allows to create stateful view-agnostic components. It means, that we can share same logic between components that look (or even behave) differently.
Consider the following simplified example:
class Stepper extends React.Component {
state = {
currentStep: 0,
};
setStep = (stepIndex) => {
const { stepCount } = this.props;
if (stepIndex < 0 || stepIndex >= stepCount) {
return;
}
this.setState({
currentStep: stepIndex,
});
};
render() {
const { currentStep } = this.state;
const { children } = this.props;
return children({
currentStep,
setStep: this.setStep,
});
}
}
Note, that render
method doesn’t actually render anything itself -
it just returns what children
function returns.
This makes Stepper
component platform-agnostic and is
especially useful when sharing same logic between React and React Native:
// React
<Stepper stepCount={3}>
{({ currentStep, setStep }) => (
<div>
<button onClick={() => setStep(currentStep - 1)}>Previous step</button>
<span>Current step is {currentStep}</span>
<button onClick={() => setStep(currentStep + 1)}>Next step</button>
</div>
)}
</Stepper>
// React Native
<Stepper stepCount={3}>
{({ currentStep, setStep }) => (
<View>
<Button title="Previous step" onPress={() => setStep(currentStep - 1)} />
<Text>Current step is {currentStep}</Text>
<Button title="Next step" onPress={() => setStep(currentStep + 1)} />
</View>
)}
</Stepper>
Use PropTypes
Describe component API using PropTypes.
It’s easy to use and can be helpful in development - you’ll get warnings
in console, if propTypes
and actual props do not match.
Also, it’s some kind of component API documentation - if
propTypes
are specified, looking at them is usually
enough to understand how to use component.
Always define defaultProps
for optional (not-required) props.
Good default value is the one, which is used most of time.
If prop is optional, but has no meaningful default value -
set its default value to null
or {}
, for example:
null
for slots (orchildren
){}
for grouped props
See PropTypes docs for more info.
Naming component props
When naming component props, try to find the nearest platform equivalent for it (e.g. HTML) and design your API similarly to platform API.
Some examples:
// ⚠️ Prop name is too specific
<SubmitButton onSubmit={...} />
// ✅ Web - similar to HTML `<button>`
<SubmitButton onClick={...} />
// ✅ React Native - similar to `Button`
<SubmitButton onPress={...} />
// ⚠️ This doesn't look bad, but maybe we can name it better?
<Dialog isOpened={...} />
// ✅ Web - similar to HTML `<dialog>`
<Dialog open={...} />
// ✅ React Native - similar to `Modal`
<Dialog visible={...} />
// ⚠️ Those props names are too specific
<PasswordInput
password={...}
onPasswordChange={...}
/>
// ✅ Web - similar to HTML `<input>`
<PasswordInput
value={...}
onChange={...}
/>
// ✅ React Native - similar to `TextInput`
<PasswordInput
value={...}
onChangeText={...}
/>
The biggest benefit of naming consistency is intuitiveness. It’s easier to get familiar with other components and remember props names.
Note, that it’s not always possible to find component’s platform equivalent. Or sometimes platform’s API is not the best example when it comes to naming. But it’s always worth to draw parallels with platform/third-party libraries.
Naming controlled components props
Most controlled components receive value and handler,
which is called when that value changes. It’s a good practice to name those
props value
and onChange
- this way naming will be consistent across
different components (or even projects).
This is one of design approaches, used by material-ui
.
I strongly recommend to check out their component API - that’s
a great place for inspiration and learning useful tricks.
Use React Hooks
Get familiar with React Hooks. They are less error-prone and easier to understand, allow to split logic by context instead of splitting logic by lifecycle hooks. This makes reusing logic between components way easier than using HOCs.
Do not rewrite your app to React Hooks. Instead, adopt them incrementally in new components.
Written by Andrew Cherniavskii, Software Engineer.