This content originally appeared on DEV Community and was authored by Francesco Di Donato
The Compound Pattern allows you to associate one or more sub-components to a component. These can be repeated and reallocated. Above all, they allow you to encapulate the structure, style and logic relating to a portion of the UI.
The High Order Component is the extension in the React context of the High Order Function. Basically it is a function that wraps a component and enhances it and/or injects additional functionality.
Have you ever tried to use the second over the first? If so, you will have realized that React will complain. I'll tell you more - he's right.
Steps
- Create Compound Component (more)
- Create High Order Component (more)
- Merging... it fails!
- Reasoning to the solution
- Abstract away the problem
If you are already aware of both patterns skip to step 3
To better understand the problem, therefore the solution, we use some code. These are deliberately simple components, precisely because I hope the focus of attention falls on how they connect rather than on what they do.
1. Create Compound Component
A Card
component to be used in the following way:
<Card>
<Card.Header>Riso, Patate e Cozze</Card.Header>
<Card.Body more buy>
<h6>Ingredients:</h6>
<ul>
<li>Rice</li>
<li>Potatoes</li>
<li>Mussels</li>
</ul>
</Card.Body>
</Card>
Implemented like this:
function Card({ children }) {
return <article>{children}</article>
}
function Header({ children }) {
return (
<header>
<h4>{children}</h4>
</header>
)
}
function Body({ children }) { ... }
Card.Header = Header // The magic of Compound Pattern
Card.Body = Body // is all here
export default Card
Create High Order Component (HOC)
A HOC can do it all. It can wrap a component with a Provider, a Router, it can also just add color here and there or even completely distort its props. For simplicity, our withAnalytics
will simply print a specific prop of the wrapped component to the console.
function withAnalytics(Component) {
return function WrappedComponent(props) {
// mock analytics call, add props as payload, etc.
console.log('Send Analytics', JSON.stringify(props.analytics))
return <Component {...props} />
}
}
export default withAnalytics
And where Card
is used we add:
<Card analytics={{ id: '123', name: 'rpc' }}>
3. Merging... it fails!
All the pieces are there. We just need to wrap Card
withwithAnalytics
.
export default withAnalytics(Card)
And crash! So many errors in console!
Let's try to remove the sub-components in Card
.
<Card analytics={{ id: '123', name: 'rpc' }}>
{/* <Card.Header>Riso, Patate e Cozze</Card.Header>
<Card.Body more buy>
<h6>Ingredients</h6>
<ul>
<li>Rice</li>
<li>Potatoes</li>
<li>Cozze</li>
</ul>
</Card.Body> */}
</Card>
The error went away. So it's something to do with assigning sub-components as static properties on Card
.
Let's analyze the Card
export.
Previously it was export default Card
. So we were exporting a function, Card
, with the associated Header
and Body
.
It is now export default withAnalytics(Card)
. We are exporting what the withAnalytics
function returns. And what is it about?
function withAnalytics(Component) {
return function WrappedComponent(props) {
console.log('Send Analytics', JSON.stringify(props.analytics))
return <Component {...props} />
}
}
It's a function, WrappedComponent
, which accepts props... wait a minute, it's a component! Not only that - it is the component we have in our hands where we import it.
Here's the problem! Because of the HOC, where we use <Card>
we are not referring to function Card()
(the one defined at step 1), but to funtion WrappedComponent
!
It is on it that we should define the sub-components!
4. Reasoning to the solution
We can't do something like:
WrappedComponent.Header = Header
Or rather: it is what we need to happen, but it must happen dynamically. Just enable withAnalytics
to receive a set of sub-components from the file that uses it.
function withAnalytics(Component, compounds) {
function WrappedComponent(props) {
console.log('Send Analytics', JSON.stringify(props.analytics))
return <Component {...props} />
}
Object.entries(compounds).forEach(([name, component]) => {
WrappedComponent[name] = component
})
return WrappedComponent
}
And where we export Card
:
export default withAnalytics(Card, { Header, Body })
Since withAnalytics
does not know how many compounds to attach to theWrappedComponent
, nor the name, it is sufficient to iterate for each of them and exploit the structure {'componentname': 'actual component'}
.
If that doesn't work, just print the
name
andcomponent
inside theforEach
.
Done. Now you can use the HOC on a component built using Compound Pattern.
But, if you feel like it, there is more.
5. Abstract away the problem
Is it possible to abstract away the sub-component assignment so that the body function of any High Order Component is concerned only with its own functionality? Yes.
We build a decorator whose purpose is to make dependencies injection of the various compounds. In this way when we build a HOC we don't have to worry about managing the compounds when we want to use it on a component created with compound pattern.
function decorateHOCWithStaticProps(hoc) {
return function getCompounds(compounds) {
return function execHOC(Component) {
const c = hoc(Component)
Object.entries(compounds).forEach(([name, component]) => {
c[name] = component
})
return c
}
}
}
We report withAnalytics
to deal only with its issues. It no longer handles compounds
. Rollback!
function withAnalytics(Component) {
return function WrappedComponent(props) {
console.log('Send Analytics', JSON.stringify(props.analytics))
return <Component {...props} />
}
}
export default withAnalytics
We keep exporting
withAnalytics
as default because it is sufficient as is when we want to apply it on a "Non-Compound Component".
When instead we want to apply it on a Compound Component:
export default withAnalytics
export const withAnalyticsCompound = decorateHOCWithStaticProps(withAnalytics)
Our HOC,
withAnalytics
, is stored insidedecorateHOCWithStaticProps
. ThewithAnalyticsCompound
variable therefore corresponds to thegetCompounds
function.
Where we define and export the Compound Component Card
:
import { withAnalyticsCompound } from 'somewhere'
function Card({ children }) { ... }
const withAnalyticsSpecial = withAnalyticsCompound({ Header, Body })
The withAnalyticsSpecial
variable corresponds to the execHOC
function.
Ideally, all this abstraction just serves to be able to call it
withAnalytics
while ignoring the problem. However I opted for the-Special
suffix to highlight that it is not the "simple" HOC default exported.
export default withAnalyticsSpecial(Card)
Before being exported, it is executed. We make explicit the values passed in the various steps:
function decorateHOCWithStaticProps(hoc) {
// where hoc = withAnalytics
return function getCompounds(compounds) {
// where compounds = { Header, Body }
return function execHOC(Component) {
// where Component = Card
const c = hoc(Component)
// wrap Card with withAnalytics but, before returning it,
// decorate it:
// c.Header = Header
// c['Body'] = Body
Object.entries(compounds).forEach(([name, component]) => {
c[name] = component
})
return c
}
}
}
In this way we have abstracted the resolution of the problem, solving it once and for all.
When you create a HOC and you want to make sure that it can also be used on Compound Components you just need:
- In addition to the default, also export a version of the HOC processed by
decorateHOCWithStaticProps
- Where you export the Compound Component, import the previous one and invoke it passing the sub-components.
- Forget about the problem and export wrapping with the result of the previous one
Contacts
Hope you find all of this useful. If you feel like it, let's get in touch!
This content originally appeared on DEV Community and was authored by Francesco Di Donato
Francesco Di Donato | Sciencx (2021-07-09T20:11:31+00:00) Merge High Order Component and Compound Pattern. Retrieved from https://www.scien.cx/2021/07/09/merge-high-order-component-and-compound-pattern/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.