Create reusable high-order React components with Recompose

January 26, 2018 0 Comments

Another webdev blog

 

 

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".)

Phone number input result

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:


const forbiddenCharactersInPhoneNumber = /[^\d\s\-()]/g const formatPhoneNumber = value => value.replace(/^(\d{3})(\d{3})(\d{4})$/, '($1) $2-$3') 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 => { if (props.onChange) { const value = event.target.value const cleanValue = value.replace(forbiddenCharsRegexp, '') const newEvent = { ...event, target: { ...event.target, value: cleanValue } } 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', 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 => { 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) } }
)

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:


Tag cloud