Create reusable high-order React components with Recompose
Posted on January 22, 2018·
Writing reusable components in React is something every React developer care about (or should care about). Wether it’s to create a toolbox of styled components, to mutualize common behaviors, etc.
Although I frequently use high-order components (with React-Redux for instance), it’s only recently that I heard about the great opportunity they offer to write reusable components more easilly, especially thanks to a great library: Recompose. Let’s see how with a concrete and complete example.
If you’ve never heard of high-order components (HOC) before, just know that basically, a HOC is a function that takes a component definition (class or function) as parameter, and returns a new component definition, that adds some behavior to the first one. It’s actually the pattern Decorator, applied to React components.
React’s website has a really complete page if you want to know more about HOCs.
A really simple example:
const addBorder = (borderWidth) => (Component) => (props) => (
<div style={{ borderColor: 'black', borderStyle: 'solid', borderWidth }}>
<Component {...props} />
</div>
)
const MyText = <p>Hello!</p>
const MyTextWithBorder = addBorder(5)(MyText)
You’ll get a component MyTextWithBorder
that will show the text "Hello!" with
a border of 5 pixels. Here, addBorder
is what is called a high-order
component.
What’s the interest of HOC? Well a really useful pattern is to extract a
behavior shared with several components into reusable functions. If you used
React with Redux and React-Redux, you probably used the HOC connect
to map
state and actions to props.
As a complete example for this article, we’ll use HOC to create a phone number input component, that will:
- accept only digits, parenthesis, dashes and spaces as input (when the user types them);
- format the phone number when the user leaves the input (on blur event). (We’ll handle only North-American phone numbers: "(514) 555-0199".)
Note that we suppose that our input will be controlled, i.e. will use value
and onChange
attributes to know the text to display and how to update it. We
also want the value to contain only the phone number digits ("5145550199"),
without caring about formatting, and therefor the onChange
callback to be
called with digits only (in event.target.value
).
To make our HOC easier to write and maintain, we’ll use the library Recompose, which offers a great number of utility functions to create HOC. We’ll see some of these in the article.
To develop our component, we’ll create two reusable HOC, one for each of the two points above. This means we’ll want our final component to defined as:
const PhoneNumberInput = formatPhoneNumber(
forbidNonPhoneNumberCharacters((props) => <input {...props} />)
)
This is a good place to introduce the first Recompose function we’ll use:
compose
. It composes several HOC to merge them into one, so we can write
something much clearer:
const PhoneNumberInput = compose(
formatPhoneNumber,
forbidNonPhoneNumberCharacters
)((props) => <input {...props} />)
And because we’ll want our HOC to be as reusable as possible (to format something other than phone numbers for instance), we’ll make them more generic:
// Only keep digits, spaces, dashes and parenthesis
const forbiddenCharactersInPhoneNumber = /[^\d\s\-()]/g
// '5145551234' => '(514) 555-1234'
const formatPhoneNumber = (value) =>
value.replace(/^(\d{3})(\d{3})(\d{4})$/, '($1) $2-$3')
// '(514) 555-1234' => '5145551234'
const parsePhoneNumber = (formattedPhoneNumber) =>
formattedPhoneNumber.replace(/[^\d]/g, '').slice(0, 10)
const PhoneNumberInput = compose(
formatInputValue({
formatValue: formatPhoneNumber,
parseValue: parsePhoneNumber,
}),
forbidCharacters(forbiddenCharactersInPhoneNumber)
)((props) => <input {...props} />)
Can you already see how this will become awesome, if we can reuse only our two HOC to format money amounts, social security numbers, and everything else, just by using the right parameters? 😉
The really interesting point is that here I use the base <input>
component,
but we could also use any component, as long as it uses value
, onChange
and
onBlur
. So we can imagine using our phone number input with React Native, or
Material-UI, etc.
Okay, now comes the important part, writing our two HOC using Recompose functions.
First HOC: only accept some characters
The idea here is that when the input value is changed (onChange
event), we’ll
intercept this event to remove every forbidden characters from the value, then
call parent onChange
with the clean value.
Here we’ll use withHandlers
function to add new event handlers as props to the
encapsulated component. The good thing is that we have access to our component
props (here we’ll use onChange
) to create our handler:
const forbidCharacters = (forbiddenCharsRegexp) =>
withHandlers({
onChange: (props) => (event) => {
// Remember `onChange` prop is not required (even if
// here nothing would happen if it´s not defined).
if (props.onChange) {
const value = event.target.value
const cleanValue = value.replace(forbiddenCharsRegexp, '')
// We don’t mutate original event, but we clone it and
// redefine the event.target.value with cleaned value.
const newEvent = {
...event,
target: { ...event.target, value: cleanValue },
}
// We dispatch our event to parent `onChange`.
props.onChange(newEvent)
}
},
})
Remember that as much as possible the component we create from another one must be compliant with the first one in its interface. It should accept the same properties with the same types.
Now if we want for instance to create a number field that will accept only digits, we can write:
const NumericField = forbidCharacters(/[^\d]/g)((props) => <input {...props} />)
We now have our first HOC to forbid some characters, now let’s write the second one, a bit more complex, to format user input.
Second HOC: format input value
For our second HOC, we’ll have to use a local inner state to store the input value without giving it to the encapsulating component. Remember we want to format the input value only when the focus is lost (blur event).
Recompose has a very simple function to add a local state to a component:
withState
. It takes as parameter the name of state attribute (that will be
given as prop to child component), the name of the function prop to update this
state attribute (also given as prop), and its initial value (static value, or a
function taking props as parameter and returning the value).
To add our state we’ll write:
withState(
'inputValue',
'setInputValue',
// formatValue is one of our HOC parameters
(props) => formatValue(props.value)
)
Easy, right? 😉
Now that we have our state, we must use update it when the input value is
changed, so we’ll define a custom onChange
handler:
withHandlers({
onChange: props => event => {
props.setInputValue(event.target.value)
},
// ...
And on blur event, we’ll format the value, call parent onChange
and onBlur
props, and update the displayed value with for formatted value:
// ...
onBlur: props => event => {
// parseValue is the other parameter of our HOC
const parsedValue = parseValue(props.inputValue)
const formattedValue = formatValue(parsedValue)
props.setInputValue(formattedValue)
// We don’t mutate original event, but we clone it and
// redefine the event.target.value with cleaned value.
const newEvent = {
...event,
target: { ...event.target, value: parsedValue }
}
if (props.onChange) {
props.onChange(newEvent)
}
if (props.onBlur) {
props.onBlur(newEvent)
}
}
)
The last step for our HOC is to ensure that only the props accepted by <input>
component will be passed to it. To do so, we’ll use Recompose’s mapProps
function to create a new prop object from existing props, and also lodash’s
omit
function to exclude some properties from an object to create a new one:
mapProps((props) => ({
...omit(props, ['inputValue', 'setInputValue']),
value: props.inputValue,
}))
Assembling everything with compose
, we’ll get:
const formatInputValue = ({ formatValue, parseValue }) =>
compose(
withState('inputValue', 'setInputValue', (props) =>
formatValue(props.value)
),
withHandlers({
onChange: (props) => (event) => {
props.setInputValue(event.target.value)
},
onBlur: (props) => (event) => {
const parsedValue = parseValue(props.inputValue)
const formattedValue = formatValue(parsedValue)
props.setInputValue(formattedValue)
const newEvent = {
...event,
target: { ...event.target, value: parsedValue },
}
if (props.onChange) {
props.onChange(newEvent)
}
if (props.onBlur) {
props.onBlur(newEvent)
}
},
}),
mapProps((props) => ({
...omit(props, ['inputValue', 'setInputValue']),
value: props.inputValue,
}))
)
That’s it! We have our two high-order components, we can use them to create our phone input field component! Below you can find the JSFiddle containing the complete source code for this example, and test the result. Don’t hesitate to fork the JSFiddle to play with Recompose or create your own high-order components.
I hope this article made you want to know more about Recompose and high-order components in general. I’m convinced HOCs create a new way to write reusable components; no doubt we’ll here about them more and more in the future 😀.
Some ressources to go further:
- Recompose API documentation is quite complete, although in my opinion it lacks some example to understand some complexe functions;
- React page about HOCs contains a lot of information, for instance what you shouldn’t do with HOCS 😉
- React Higher Order Components in depth: a great introduction to HOCs
- Why The Hipsters Recompose Everything: a cool intro to concepts of Recompose (seems a little outdated…)
Check my latest articles
- 📄 13 tips for better Pull Requests and Code Review (October 17, 2023)Would you like to become better at crafting pull requests and reviewing code? Here are the 13 tips from my latest book that you can use in your daily developer activity.
- 📄 The simplest example to understand Server Actions in Next.js (August 3, 2023)Server Actions are a new feature in Next.js. The first time I heard about them, they didn’t seem very intuitive to me. Now that I’m a bit more used to them, let me contribute to making them easier to understand.
- 📄 Intro to React Server Components and Actions with Next.js (July 3, 2023)React is living something these days. Although it was created as a client UI library, it can now be used to generate almost everything from the server. And we get a lot from this change, especially when coupled with Next.js. Let’s use Server Components and Actions to build something fun: a guestbook.