In React/Typescript it is common to extend or decorate an existing component to add some features or tweak its behavior. When doing so composition is a common (and recommended) pattern. But when doing so, how do you pass arbitrary props to the inner component?
Inheritance by Composition
This is a classic pattern, well used and for a good reason. Instead of directly inheriting the component you want to modify, you create a simple wrapper around it. This provides encapsulation as well as control.
Example component
Typically an ‘ordinary’ component like a Button, ListItem or similar is extended.
export type MyCustomButtonProps = {
isBusy: boolean;
};
export const MyCustomButton: FC<MyCustomButtonProps> = props => {
const { isBusy } = props;
return (
<Button disabled={isBusy}>
'Click me'
</Button>
);
};
Example usage
...
return (
<h1>Application status</h1>
<MyCustomButton isBusy={appIsBusy} />
);
The props challenge
Properties of the wrapped component should still be available to users of your wrapper. What you want to do is to forward the properties transparently from your wrapper directly to the wrapped component. When doing so you don’t want to mimic all properties the wrapped component has in your own custom component. Like “isEnabled”, “title” and so on. There’s an easy way to do that.
Wrapper component with ‘transparent’ prop handling
export type MyCustomButtonProps = {
isBusy: boolean;
};
export const MyCustomButton: FC<MyCustomButtonProps & ButtonProps> = props => {
const { isBusy, ...buttonProps } = props;
return (
<Button {...buttonProps} disabled={isBusy}>
'Click me'
</Button>
);
};
Notice the ampersand in the FC generic component parameters: this is where the wrapped component properties are ‘merged’ with your wrapper component properties. We destructure the properties with the spread operator, and can then pass the props transparently to the wrapped component.
This way the existing properties on the component you wrap can simply be forwarded, without re-declaring them, like this:
Usage
...
return (
<h1>Application status</h1>
<MyCustomButton isBusy={appIsBusy} onClick={handleButtonClick} />
);
In the example above we hook up an event handler that is on the wrapped component (Button), without having to redeclare it in our custom component (MyCustomButton). Nice and easy!