December 6, 2020

Using Higher Order Components and Render Highjacking to Inject Props to Children

In this post, we are going to learn how to build two HOCs (higher order components) withClassName and withStyle which use render highjacking to manipulate the wrapped components's children's props.

Take a look at the code for this simple Button component:

1type Props = {
2 text: string;
3 onClick: () => void;
4};
5
6const Button: React.FC<Props> = ({ children, text, onClick }) => {
7 return <button className="rounded-md p-3">{children}</button>;
8};

Now let's say we are trying to build another component called BlueButton and we want to reuse our existing Button component to accomplish that. In order to do that, let's make our Button accept a className prop and pass in the appropriate class names to make this button blue.

1type Props = {
2 text: string;
3 onClick: () => void;
4 className?: string;
5};
6
7const Button: React.FC<Props> = ({ children, text, onClick, className }) => {
8 const originalClassName = 'p-3 rounded-md';
9 const finalClassName = className ? `${originalClassName} ${className}`;
10 return <button className="p-3 rounded-md">{children}</button>;
11};

We can now build our BlueButton component:

1type Props = {
2 text: string;
3 onClick: () => void;
4};
5
6const BlueButton: React.FC<Props> = (props) => (
7 <Button {...props} className="bg-blue-700 text-gray-300" />
8);

That was a lot of typing. I've found this to be a recurring pattern when developing react applications. Especially for things like margin which in my opinion are not worth making them their own prop and they just make our code messy. So I wrote a little utility function that can do this for me. Let's take a look at the code:

1// "W" is the wrapped component and "E" is the enhanced component (what we are returning)
2const withClassName = <T extends { [x: string]: any }>(W: React.FC<T>) => {
3 const E: React.FC<T & { className?: string }> = ({
4 className,
5 ...wrappedProps
6 }) => {
7 const elementsTree = W(wrappedProps as T);
8 if (!elementsTree) return null;
9 if (!className) return elementsTree;
10
11 const newClassName = elementsTree.props.className
12 ? `${elementsTree.props.className} ${className}`
13 : className;
14
15 return React.cloneElement(
16 elementsTree,
17 { ...elementsTree.props, className: newClassName },
18 elementsTree.props.children,
19 );
20 };
21
22 return E;
23};

I've used React.cloneElement API to inject my className props to the elementsTree resulted by rendering the wrapped component. What I like most about this is that it's type-safe which makes the composed component accept all the wrapped component's props plus the additional className prop. I'm accepting wrapped props as a generic type parameter T and returning a function component of type React.FC<T & { className?: string; }. Now we can build our BlueButton component without touching the Button component.

1import withClassName from "utils/withClassName";
2
3import _Button from "components/Button";
4
5const Button = withClassName(_Button);
6
7type Props = {
8 text: string;
9 onClick: () => void;
10};
11
12const BlueButton: React.FC<Props> = (props) => (
13 <Button {...props} className="bg-blue-700 text-gray-300" />
14);

How clean is that?! 😅

We can do something similar for the style prop. Check out the snippet below:

1// "W" is the wrapped component and "E" is the enhanced component (what we are returning)
2const withStyle = <T extends { [x: string]: any }>(W: React.FC<T>) => {
3 const E: React.FC<T & { style?: React.CSSProperties }> = ({
4 style,
5 ...wrappedProps
6 }) => {
7 const elementsTree = W(wrappedProps as T);
8 if (!elementsTree) return null;
9 if (!style) return elementsTree;
10
11 return React.cloneElement(
12 elementsTree,
13 {
14 ...elementsTree.props,
15 style: { ...elementsTree.props.style, ...style },
16 },
17 elementsTree.props.children,
18 );
19 };
20
21 return E;
22};

If you have never written you own HOC, hope this post inspires you to write one once you find the right use case.

Happy Coding!