React: different ways to enhance child components

Sho Mukai
4 min readJan 12, 2021

--

When designing React components, there are cases we want to change how the child components work, usually this is done by passing in different props. This may duplicate some common logic in different child component types. In this case, we can uplift the logic into a common place by using higher order components.

But sometimes, we can’t use HOC easily. For example when creating a React lib, to make it easy to use, we don’t want to force user to create something special, just let user pass in whatever they want. There are several other ways to do the uplifting.

Use React API cloneElement()

This method can copy the child and add new props into it.

React.cloneElement(element, newProps, children);

Similar to below:

<element.type {...element.props} {...newProps}>{children}</element.type>

The props.children in React parent component is already an array of React element instances, after running createElement(element.type, props, children) , so basically we can’t change anything of it. By cloneElement() , we can recreate the children with new props.

For example, if we want to create an image gallery lib, when user clicks any of the child image, we want to highlight it. The JSX may look like this:

<Gallery>
<img src="1">
<DecoratedImg src="2">
</Gallery>

To change the style, we can pass in a style prop into the image and also we can add an onClick handler to change the gallery state.

<GalleryWrapper>
{React.Children.map(props.children, child => {
return React.cloneElement(child, {
onClick: onImgClick,
style: { box-shadow: xxxx }
});
})}
</GalleryWrapper>

Use custom hooks

With the ability to extract component logic into reusable functions, this is quite popular now.

For example, the same Gallery lib can be done like below:

function Gallery () {
// render Gallery with context
return (
<GalleryContext.Provider value={{ onClick, style }}>
{children}
</GalleryContext.Provider>
);
};
const useAsGalleryImage = () => {
// call any other hooks or any other common logic
const { extraProps, onClick, style } = useContext(GalleryContext);
return {
...extraProps,
onClick,
style
};
};

Here, GalleryContext is used to provide common functions for each children. Then in the child image component, we can use this custom hook:

const GalleryImage = ({ src }) => {
// Call the custom hook and get props
const galleryProps = useAsGalleryImage();
return (
<img src={src} {...galleryProps} />
);
}

Use render function or make children a function call

In this way, we can define the interface between child and parent, then call the function to render children with props derived from current parent state. A good example of this is react-router:

<Route
{...rest}
render={routeProps => (
<FadeIn>
<Component {...routeProps} />
</FadeIn>
)}
/>

Passing in children as a function call:

<Gallery>
{(getProps, style) => {
// Render logic here
return images.map((image) => {
return <img {...getProps()} style={style} />
});
}}
</Gallery>

In parent component, the logic is similar to below:

const Parent = ({ children, render }) => {
// Process to create props
const getProps = () => {
// props derived from current state
};
// If render function prop is passed in
if (render) {
return render({ ...getProps(), style });
}
// We can add more check for children type, before calling it
return children(getProps, style);
};

As long as the React component returns something that is a React.Node type, what React can render, it will work fine.

type Node = React.ChildrenArray<void | null | boolean | string | number | React.Element<any>>;

Similarly, we can change the children layout by passing in a layout function.

function ChildComponent ({ layout }) => {
const header = <HeaderComponent />;
const body = <BodyComponent />;
return layout(header, body);
}
function ChildComponentInModal () {
return (
<ChildComponent
layout={(header, body) => {
return (
<Modal>
<ModalHeader>
{header}
</ModalHeader>
{body}
</Modal>
);
}}
/>
);
};
function ChildComponentInPage () {
return (
<ChildComponent
layout={(header, body) => {
return (
<PageLayout>
<Header>
{header}
</Header>
<Body>
{body}
</Body>
</PageLayout>
);
}}
/>
);
};

Use child component wrapper

This is similar to the higher order components pattern.

function Gallery () {
// render Gallery with context
return (
<GalleryContext.Provider value={{ onClick, style }}>
{children}
</GalleryContext.Provider>
);
};
function Image ({ children }) {
const { extraProps, style } = useContext(GalleryContext);
// render a children wrapper or use React.cloneElement()
return (
<div {...extraProps} style={style}>
{children}
</div>
);
}
Gallery.Image = Image;<Gallery>
<Gallery.Image>
<img src="1" />
</Gallery.Image>
<Gallery.Image>
<img src="2" />
</Gallery.Image>
</Gallery>

Conclusion

Using JavaScript instead of a template language to render views makes React much easier to use. Thanks to this, we can pass around JavaScript functions as parameters and use the already existing JS function scope. This leads to clean view code, and amazing libs for various features, like react-router, react-use and many others.

Hope the methods listed above can help when creating React components.

--

--