React HOCs - Hooks Version
React HOCs explained in the functional components way.
- React HOCs are a pattern to abstract and share some logic accross many components.
- HOCs use containers as part of their implementation.
- You can think of HOCs as parameterized container component definitions.
- A React HOC is a pure function that... :
- Takes a component.
- Defines some functionality.
- Returns one of the two:
- A new component based on the given one, but with the additional functionality, or:
- Another HOC.
Say you have many different components that use the same pattern to subscribe to an external data source to render the fetched data, e.g.:
const SubscribingUsersList = () => {
const [users, setUsers] = useState([])
const onChange = useCallback(() => DS.getUsers().then(data => setUsers(data)))
useEffect(() => {
onChange()
DS.addChangeListener(onChange)
return () => DS.removeChangeListener(onChange)
}, [])
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}
const SubscribingBlogPost = ({ postId }) => {
const [post, setPost] = useState(null)
const onChange = useCallback(() => DS.getPost(postId).then(data => setPost(data)))
useEffect(/* Same effect... */)
return (
<div>
<h6>{post?.title}</h6>
<pre>{post?.body}</pre>
</div>
)
}
...
So, let's use a HOC to abstract this fetch and subscription logic and share it across many components, like simpler versions of the above.
Our HOC will... :
- Accept as one of its arguments a child component.
- Create a new component that... :
- Wrapps the given component.
- Subscribes to
DS
. - Passes subscribed data as a prop to the given component.
const withSubscription = (Component, fetcher) => props => {
const [data, setData] = useState(null)
const onChange = useCallback(() => fetcher(DS, props).then(json => setData(json))
useEffect(/* Same effect... */)
return <Component data={data} {...props} />
}
Now let's make new simple versions of our components, that don't manage any subscriptions, and only provide UI.
We'll expose the new subscribing version of them using withSubscription
:
Inside UsersList.js
:
const UsersList = ({ data }) => {
return (
<ul className='UsersList'>
{data?.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}
const usersFetcher = DS => DS.getUsers()
export default withSubscription(UsersList, usersFetcher)
Inside BlogPost.js
:
const BlogPost = ({ data }) => {
return (
<div className='BlogPost'>
<h6>{data?.title}</h6>
<pre>{data?.body}</pre>
</div>
)
}
const postFetcher = (DS, { postId }) => DS.getPost(postId)
export default withSubscription(BlogPost, postFetcher)
Nice!
To control the name of the passed prop, we could do as follows:
In withSubscription
:
const withSubscription = (Component, fetcher, passedPropName = 'data') => props => {
...
return <Component {...{ [passedPropName]: data }} {...props} />
}
(Note the new passedPropName
argument and the new way we pass data
using passedPropName
)
In UsersList.js
:
const UsersList = ({ users }) => {
return (
<ul>
{users?.map(user => (
...
))}
</ul>
)
}
...
export default withSubscription(UsersList, usersFetcher, 'users')
(Note the users
prop in place of data
and the new 'users'
parameter)
In BlogPost.js
:
const BlogPost = ({ post }) => {
...
}
export default withSubscription(BlogPost, postFetcher, 'post')
(Same story - post
instead of data
and an additional 'post'
parameter)
- Don’t Mutate the Original Component. Use Composition.
- Convention: Pass Unrelated Props Through to the Wrapped Component.
- Convention: Maximizing Composability.
- Convention: Wrap the Display Name for Easy Debugging.
- Static Methods Must Be Copied Over.
- Refs Aren’t Passed Through.
That’s because
ref
is not really a prop — likekey
, it’s handled specially by React. If you add aref
to an element whose component is the result of a HOC, theref
refers to an instance of the outermost container component, not the wrapped component. The solution for this problem is to use theReact.forwardRef
API.
const getDisplayName = component => component.displayName || component.name || 'Component'
const withSubscription = (Component, fetcher, passedPropName = 'data') => {
const WithSubscription = props => {
...
}
WithSubscription.displayName = `WithSubscription(${getDisplayName(Component)})`
return WithSubscription
}
...
import hoistNonReactStatic from 'hoist-non-react-statics'
const getDisplayName = ...
const withSubscription = (Component, fetcher, passedPropName = 'data') => {
const WithSubscription = props => {
...
}
hoistNonReactStatic(WithSubscription, Component)
...
}