December 6, 2020
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:
Copied!1type Props = {2 text: string;3 onClick: () => void;4};56const 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.
Copied!1type Props = {2 text: string;3 onClick: () => void;4 className?: string;5};67const 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:
Copied!1type Props = {2 text: string;3 onClick: () => void;4};56const 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:
Copied!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 ...wrappedProps6 }) => {7 const elementsTree = W(wrappedProps as T);8 if (!elementsTree) return null;9 if (!className) return elementsTree;1011 const newClassName = elementsTree.props.className12 ? `${elementsTree.props.className} ${className}`13 : className;1415 return React.cloneElement(16 elementsTree,17 { ...elementsTree.props, className: newClassName },18 elementsTree.props.children,19 );20 };2122 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.
Copied!1import withClassName from "utils/withClassName";23import _Button from "components/Button";45const Button = withClassName(_Button);67type Props = {8 text: string;9 onClick: () => void;10};1112const 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:
Copied!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 ...wrappedProps6 }) => {7 const elementsTree = W(wrappedProps as T);8 if (!elementsTree) return null;9 if (!style) return elementsTree;1011 return React.cloneElement(12 elementsTree,13 {14 ...elementsTree.props,15 style: { ...elementsTree.props.style, ...style },16 },17 elementsTree.props.children,18 );19 };2021 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!