The front-end ecosystem has been evolving extremely rapidly, with the rise of frameworks like React, Vue, and Angular leading to dramatic changes in application architectures and a whole new set of patterns for JavaScript developers to learn.
With Wordpress embracing React in the new Gutenberg editor, millions of developers are suddenly being introduced to this world, and scrambling to catch up.
In this post we're going to break down one of those architectural patterns that is extremely common in React - higher-order components.
A friend recently asked for help understanding a new utility added to Gutenberg for manipulating colors, the withColors
higher order component. You can see an example here, reproduced below:
edit: withColors( 'backgroundColor', { textColor: 'color' } )(
function( props ) {
// Props added by withColors HOC.
var backgroundColor = props.backgroundColor;
var setBackgroundColor = props.setBackgroundColor;
var textColor = props.textColor;
var setTextColor = props.setTextColor;
// Class computation
var paragraphClasses = (
( backgroundColor.class || '' ) + '' + ( textColor.class || '' )
).trim();
return el( Fragment, {},
el( 'p', {
className: paragraphClasses,
style: {
backgroundColor: backgroundColor.value,
color: textColor.value,
}
},
'Hello world'
),
el( InspectorControls, {},
el( PanelColor, {
colorValue: backgroundColor.value,
title: __( 'Background Color' ),
onChange: setBackgroundColor,
} ),
el( PanelColor, {
colorValue: textColor.value,
title: __( 'Text Color' ),
onChange: setTextColor,
} )
)
);
}
),
This feels a little intimidating - what exactly is going on? The withColors
function is implementing a pattern called a higher-order component. Let's break down what that means:
Higher-Order Components
A higher-order component (HOC) is a React concept that is a composition-centric way of encapsulating shared logic so you can use it on many components.
As defined in the React documentation:
A higher-order component is a function that takes a component and returns a new component.
This is similar in many ways to a decorator, essentially allowing you to encapsulate some reusable functions (e.g. logic for getting and setting colors) or data in a "component wrapper".
This wrapper is a function that accepts a component and returns a "wrapped" version of that component that will receive those functions and data as props
.
So instead of every component that needs to have access to color info _inheriting_ from a "colored" component, or importing a color "library" module that they need to invoke, they instead import the withColors
higher order component (HOC), "wrap" their component with that component, and now their component will receive props that have to do with color along with any other props it specifically defines.
The Bare Minimum Higher-Order Component
To make this as simple as possible, lets look at a bare minimum HOC.
A bare minimum HOC would be just a function wrapping a function, e.g.
import secretKeyHOC from 'secret-key';
const BareComponent = function(props) {
//do something
}
const myComponent = secretKeyHOC(BareComponent);
export default myComponent;
Where secretKeyHOC is defined somewhere else, and might look like:
const secretKeyHOC = function(component) {
return function(props) {
return component({... props, secretKey: 'mySecretKey'});
});
}
Essentially the HOC is just merging in some new props for your component - in this bare bones example it merges in a secret key.
Higher-Order Components With Arguments
Most higher-order components are not as simple as the secret key above... they encapsulate reusable logic, but typically need to be configured in some way.
For example, our secretKey component might contain the logic for looking up one of several secret keys, but need to be configured to know which key to provide for which component.
To do this, higher-order components are often implemented as a chain of functions. First you invoke a function with configuration arguments, which then returns another function that can be used to wrap your component.
For example, if we could configure our secretKeyHOC
to pick either a github secret key or a gitlab secret key, it might look like:
const secretKeyHOC = function(keyType) {
const keys = {gitlab: 'key1', github: 'key2' }
const key = keys[keyType];
return function(component) {
return function(props) {
return component({... props, secretKey: key});
});
}
}
We would then invoke the component like this:
import secretKeyHOC from 'secret-key';
const BareComponent = function(props) {
//do something
}
const myComponent = secretKeyHOC('gitlab')(BareComponent);
export default myComponent;
First we call the function passing in our configuration arguments, then call the returned function with the component we're wrapping.
Back to withColors
Looking back at withColors
from Gutenberg, we can see in the documentation that the HOC accepts arguments as follows:
withColors HOC can receive multiple arguments. Each argument of the withColors HOC can be a string or an object.
If the argument is an object, it should just contain one key with a value.
The key should be the name of attribute where predefined colors are set e.g.: 'textColor'.
The value should be the context where this color is used e.g.: 'color'.
Looking back at how this was invoked in the example code, we see:
edit: withColors( 'backgroundColor', { textColor: 'color' } )(
function( props ) {
// Props added by withColors HOC.
var backgroundColor = props.backgroundColor;
var setBackgroundColor = props.setBackgroundColor;
var textColor = props.textColor;
var setTextColor = props.setTextColor;
// some more stuff we'll ignore
}
),
With our understanding of higher-order components, we can now see exactly what this is doing.
First, we're calling withColors
first with a couple of arguments specifying that we want backgroundColor
and textColor
. This returns a "wrapper function" (HOC) that we call passing in our underlying component - the function that will receive props.
This ensures that the component will always receive 4 props: backgroundColor
, setBackgroundColor
, textColor
, and setTextColor
, in addition to props passed in by its parent.
This "wrapped" component is what is then assigned to edit
, as the component that will be used for editing this Gutenberg block.
In this way, Gutenberg creates a clean, isolated, props-based approach to modifying and using local color changes within a block.
P.S. - If you're interested in these types of topics, you should probably follow me on Twitter or join my mailing list. I send out a weekly newsletter called the ‘Friday Frontend’. Every Friday I send out 15 links to the best articles, tutorials, and announcements in CSS/SCSS, JavaScript, and assorted other awesome Front-end News. Sign up here: https://zendev.com/friday-frontend.html
Top comments (0)