Some Tips to replace Recompose with React Hooks

It’s Takuya here. I’m the solo developer of Inkdrop, a Markdown note-taking app. It has a mobile version built with React Native. Recently…

Some Tips to replace Recompose with React Hooks

Tips to replace Recompose’s HOCs with React Hooks for better DX

It’s Takuya here. I’m the solo developer of Inkdrop, a Markdown note-taking app. It has a mobile version built with React Native. Recently I worked on refactoring its codebase, replacing Recompose with React Hooks. I would like to share my tips I found while working on that.

Too many HOCs are less maintainable

Recompose is a library bringing you a set of useful HOC(higher-order components) utility functions for building React apps efficiently with stateless functional components, made by Andrew Clark. You can imagine it like Lodash for React. The development has stopped since Dec 4, 2018 because he joined the React team to develop its new feature introduced in v16.8: React Hooks.

As they said, they don’t recommend rewriting your existing components overnight. But I did it. I have several reasons that made me rewrite my components. Recompose is a utility library that lets you build React apps heavily based on HOCs. HOC functions are reusable and you can save a lot of time to repeat writing same things. But I found that there were some drawbacks in adopting Recompose and many HOCs:

1. It messes up static typings

I’m using Flow to type-check. Recompose’s flow type definitions are heavily relied on type inference like so:import { compose, withHandlers, pure, type HOC } from 'recompose'
type Props = {}const enhance: HOC<*, Props> = compose(
 connect(({ editingNote, editor, session }) => ({
   editingNote,
   readOnly: editor.readOnly || session.isReadOnly
 })),
 pure,
 withKeyboard(),
 withHandlers({
   handleTitleFocus: _props => () => {}
 })
)const Editor = enhance(props => {
 // ...
})

But for some reason the type of props is often resolved as any and that doesn’t help check types in components. Flow usually doesn’t let me know that I’m writing an incorrect property name, etc. And I can’t see which props are available from the codebase because they all are inferred. As number of HOCs is increased, it is hard to maintain.

2. It’s hard to inspect when you use React Developer Tools

Many nested HOCs appear in Elements

As you can see in the screenshot above, you don’t get the actual component structure as there are many nested HOCs like pure(withHandlers(Component)). That makes React Developer Tools almost unusable.

3. It has a dependency on fbjs

Recompose depends on fbjs — a Facebook’s own utility library that is now obsolete. It has old libraries as its dependency as well and that makes your project unnecessarily fat and vulnerable. So I always try to avoid depending on modules that have a dependency on fbjs directly or indirectly.


I’ve already adopted React Hooks in the desktop version earlier and I found that it solves those issues discussed above very well. I feel more comfortable DX(development experience) with it. Hooks makes the codebase much cleaner. Awesome job, Andrew Clark.

Now, let’s talk about how I replaced Recompose with React Hooks. It’s easy than you’d imagine :) I guess it’d be also helpful for learning how to use Hooks in your new projects.

Make it clean and get better DX

Here are before and after of each Recompose’s usecase.

Recompose.lifecycle -> React.useEffect

Before:const PostsList = ({ posts }) => (
 <ul>{posts.map(p => <li>{p.title}</li>)}</ul>
)const PostsListWithData = lifecycle({
 componentDidMount() {
   fetchPosts().then(posts => {
     this.setState({ posts });
   })
 }
})(PostsList);

After:import { useEffect, useState } from 'react'const PostsList = () => {
 const [posts, setPosts] = useState([]);
 useEffect(() => {
   fetchPosts().then(posts => {
     setPosts(posts);
   })
 }, [])return (
   <ul>{posts.map(p => <li>{p.title}</li>)}</ul>
 )
}

Recompose.withHandlers -> useCallback

Before:const enhance = compose(
 withState('value', 'updateValue', ''),
 withHandlers({
   onChange: props => event => {
     props.updateValue(event.target.value)
   },
   onSubmit: props => event => {
     event.preventDefault()
     submitForm(props.value)
   }
 })
)const Form = enhance(({ value, onChange, onSubmit }) =>
 <form onSubmit={onSubmit}>
   <label>Value
     <input type="text" value={value} onChange={onChange} />
   </label>
 </form>
)

After:import { useState, useCallback } from 'react'const Form = () => {
 const [value, updateValue] = useState('')
 const onChange = useCallback(event => {
   updateValue(event.target.value)
 }, [updateValue])
 const onSubmit = useCallback(event => {
   event.preventDefault()
   submitForm(value)
 }, [value])return (
   <form onSubmit={onSubmit}>
     <label>Value
       <input type="text" value={value} onChange={onChange} />
     </label>
   </form>
 )
}

Recompose.withStateHandlers -> React.useState

Before:const Counter = withStateHandlers(
({ initialCounter = 0 }) => ({
 counter: initialCounter,
}),
{
 incrementOn: ({ counter }) => (value) => ({
   counter: counter + value,
 }),
 decrementOn: ({ counter }) => (value) => ({
   counter: counter - value,
 }),
 resetCounter: (_, { initialCounter = 0 }) => () => ({
   counter: initialCounter,
 }),
}
)(
({ counter, incrementOn, decrementOn, resetCounter }) =>
 <div>
   <Button onClick={() => incrementOn(2)}>Inc</Button>
   <Button onClick={() => decrementOn(3)}>Dec</Button>
   <Button onClick={resetCounter}>Reset</Button>
 </div>
)

After:import { useState, useCallback } from 'react'const Counter = () => {
 const [counter, setCounter] = useState(0)
 const incrementOn  = useCallback((value) => {
   setCounter(counter + value)
 }, [counter])
 const decrementOn = useCallback((value) => {
   setCounter(counter - value)
 }, [counter])
 const resetCounter = useCallback(() => {
   setCounter(0)
 }, [counter])return (
   <div>
     <Button onClick={useCallback(() => incrementOn(2), [counter])}>Inc</Button>
     <Button onClick={useCallback(() => decrementOn(3), [counter])}>Dec</Button>
     <Button onClick={resetCounter}>Reset</Button>
   </div>
 )
}

Recompose.pure -> React.memo

Before:const Comp = pure(props => <div>{props.message}</div>)

After:const Comp = memo(props => <div>{props.message}</div>)

Recompose.onlyUpdateForKeys -> React.memo

Before:const enhance = onlyUpdateForKeys(['title', 'content', 'author'])
const Post = enhance(({ title, content, author }) =>
 <article>
   <h1>{title}</h1>
   <h2>By {author.name}</h2>
   <div>{content}</div>
 </article>
)

After:import { memo } from 'react'const Post = memo(({ title, content, author }) => {
 return (
   <article>
     <h1>{title}</h1>
     <h2>By {author.name}</h2>
     <div>{content}</div>
   </article>
 )
}, (prevProps, nextProps) => {
 return prevProps.title === nextProps.title &&
   prevProps.content === nextProps.content &&
   prevProps.author === nextProps.author
})

From the doc: You can also add a second argument to specify a custom comparison function that takes the old and new props. If it returns true, the update is skipped.

As you might notice, React Hooks doesn’t make your code length drastically shorter than you’d expect because Recompose is already enough efficient. Now, I’ve got much cleaner view of React components on Developer Tools like so:

And this refactoring let me find some unnecessary lines of code as Flow can now work properly 🎉. I hope it’s helpful for those who have a project with Recompose!