Most of you probably know this but I want to post a quick tip for whom doesn't know. We all know about how important props in a component. And sometimes you need to default some of the props so when it is not provided you still want them to be still in place with some default values. Luckily we have static defaultProps property of a component that we can define those and it is pretty common among React components. But in this post I would like to show a different approach. So less talking more coding.. I will use Material-UI components in examples below, so don't wander where all these component names come from.
Let's say you have a compound component ( have single component in combination of multiple components ).
class LoadingButton extends React.Component {
constructor(props){
super(props);
this.state.loading = this.props.loading
}
render(){
const {loading, loadingText, loadingTextVariant, progressColor, progressThickness, disabled, ...props} = this.props;
return (
<Button
disabled={disabled || this.state.isLoading}
{...props}
onClick={(e) => {
this.setState({isLoading: true});
onClick && onClick(e);
}}
>
{!this.state.isLoading ? children : (
<React.Fragment>
<CircularProgress color={progressColor} thickness={progressThickness} />
<Typography variant={loadingTextVariant}>
{loadingText}...
</Typography>
</React.Fragment>
)}
</Button>
);
}
}
LoadingButton.defaultProps = {
progressColor: 'primary',
progressText: 'Loading...',
progressThickness: 3,
loading: false,
onClick: () => null,
loadingTextVariant: 'caption'
}
Nothing wrong with this one. You have a button component and couple other components inside. and you have different props addressing different components. But how would you handle this if there were a lot more components, and you want to default not 3 but bunch of other props or you also want to include other props developer may need to change on a sub component you used. For instance what if I also want to update size of progress? Your LoadingButton component doesn't allow me to add a new prop. So yes, I have to clone what you did and create my own which has size prop.
And here is my approach for situations like this, especially if I am sharing this with others.
class LoadingButton extends React.Component {
constructor(props){
super(props);
this.state.loading = this.props.loading
}
render(){
const {loading, loadingText, disabled, LoadingTextProps, ProgressProps, ...props} = this.props;
return (
<Button
disabled={disabled || this.state.isLoading}
onClick={(e) => {
this.setState({isLoading: true});
onClick && onClick(e);
}}
{...props}
>
{!this.state.isLoading ? children : (
<React.Fragment>
<CircularProgress {...{color: 'primary', thickness: 3, ...ProgressProps}}/>
<Typography {...{variant: 'caption' , ...LoadingTextProps}}>
{loadingText}...
</Typography>
</React.Fragment>
)}
</Button>
);
}
}
LoadingButton.propTypes = {
loading: PropTypes.bool,
loadingText: PropTypes.string,
...Button.propTypes,
LoadingTextProps: PropTypes.shape({...Typography.propTypes}),
ProgressProps: PropTypes.shape({...CircularProgress.propTypes})
};
LoadingButton.defaultProps = {
loading: false,
loadingText: 'Loading'
onClick: () => null
}
Nothing magic or special. Just a little ES6 spread operator power. By adding {...{variant: 'caption' , ...LoadingTextProps}}
I can define a default prop value to variant
but also letting developer to override if s/he wants to as well as add additional props to sub-component. less to define in defaultProps and more structured props and propTypes.
And one side note, if you are thinking why do we need to defined default properties inside the component rather than in default props like LoadingTextProps : {variant: 'caption'}
is because default props will not do deep merge when that prop is passed with the component. So if developer passes LoadingTextProps: {color: 'primary'}
our variant will be overridden and value will be null
.