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
- 📄 A better learning path for React with server components (May 26, 2023)What if we took advantage of React Server Components not only to improve how we use React, but also how we help people learn it from the beginning?
- 📄 Display a view counter on your blog with React Server Components (April 24, 2023)A short tutorial with a cool use case for React Server Components, Streaming and Suspense with Next.js: adding a view counter on a blog, calling the Plausible analytics API.
- 📄 Using Zod & TypeScript for more than user input validation (March 8, 2023)If you have ever created an API or a form accepting user input, you know what data validation is, and how tedious it can be. Fortunately, libraries can help us, such as Yup or Zod. But recently, I realized that these libraries allow patterns that go much farther than input validation. In this post, I’ll show you why I now use them in most of my TypeScript projects.