A further exploration of the Hooks, Render Props, and HOC patterns

In Part 1, I presented an approach to decouple the data fetching/management layer from the UI, which would free us from being locked into any particular data library or framework. Let’s call this Approach A.
Approach A. Custom Hook
Let’s create a custom hook — useSomeData
— that returns the properties someData
, loading
, and error
regardless of the data fetching/management logic. The following are 3 different implementations of useSomeData
.
With Fetch API and component state:
import React, { useState, useEffect } from 'react'; | |
const useSomeData = () => { | |
const [someData, setSomeData] = useState(null); | |
const [loading, setLoading] = useState(false); | |
const [error, setError] = useState(null); | |
useEffect(() => { | |
if (!someData) { | |
setLoading(true); | |
fetch('/some-data') | |
.then(response => response.json()) | |
.then(data => setSomeData(data)) | |
.catch(error => setError(error)) | |
.finally(() => setLoading(false)); | |
} | |
}, []); | |
return { someData, loading, error }; | |
}; |
With Redux:
import React, { useState, useEffect } from 'react'; | |
import { useDispatch, useSelector } from 'react-redux'; | |
import { selectSomeData } from 'path/to/data/selectors'; | |
import { fetchSomeData } from 'path/to/data/action'; | |
const useSomeData = () => { | |
const dispatch = useDispatch(); | |
const someData = useSelector(selectSomeData); | |
const [loading, setLoading] = useState(false); | |
const [error, setError] = useState(null); | |
useEffect(() => { | |
if (!someData) { | |
setLoading(true); | |
dispatch(fetchSomeData()) | |
.catch(error => setError(error)) | |
.finally(() => setLoading(false)); | |
} | |
}, []); | |
return { someData, loading, error }; | |
}; |
With Apollo GraphQL:
import { gql, useQuery } from '@apollo/client'; | |
const useSomeData = () => { | |
const { data: someData, loading, error } = useQuery(gql` | |
fetchSomeData { | |
data { | |
# some fields | |
} | |
} | |
`); | |
return { someData, loading, error }; | |
}; |
The 3 implementations above are interchangeable without having to modify this UI component:
import React from 'react'; | |
import useSomeData from 'path/to/custom-hook'; | |
const SomeComponent = (props) => { | |
const { someData, loading, error } = useSomeData(); | |
return ( | |
<React.Fragment> | |
{loading && <div>{'Loading...'}</div>} | |
{!loading && error && <div>{`Error: ${error}`}</div>} | |
{!loading && !error && someData && <div>{/* INSERT SOME AMAZING UI */}</div>} | |
</React.Fragment> | |
); | |
}; |
But, as Julius Koronci correctly pointed out, while the data fetching/management logic is decoupled, the SomeComponent
UI is still coupled to the useSomeData
hook.
In other words, even though we can reuse useSomeData
without SomeComponent
, we cannot reuse SomeComponent
without useSomeData
.
Perhaps this is where Render Props and Higher Order Components do a better job at enforcing the separation of concerns (thanks again to Julius for highlighting this).
Approach B. Render Props
Instead of a custom hook that returns someData
, loading
, and error
, let’s create a Render Props component — SomeData
— that wraps around a function (i.e., children needs to be a function), implements the data logic, and passes in someData
, loading
, and error
into the function.
import React, { useState, useEffect } from 'react'; | |
const SomeData = ({ children }) => { | |
// DATA FETCHING/MANAGEMENT FRAMEWORK OR LIBRARY OF YOUR CHOICE | |
return children({ someData, loading, error }); | |
}; | |
const SomeComponent = ({ someData, loading, error }) => ( | |
<React.Fragment> | |
{loading && <div>{'Loading...'}</div>} | |
{!loading && error && <div>{`Error: ${error}`}</div>} | |
{!loading && !error && someData && <div>{/* INSERT SOME AMAZING UI */}</div>} | |
</React.Fragment> | |
); | |
const SomeComponentWithSomeData = () => ( | |
<SomeData> | |
{renderProps => (<SomeComponent {...renderProps} />)} | |
</SomeData> | |
); |
You can replace line 4 in the snippet above with Redux, Apollo GraphQL, or any data fetching/management layer of your choice.
We can now reuse SomeComponent
(UI component) without SomeData
(Render Props component). We can also reuse SomeData
without SomeComponent
.
Approach C. Higher Order Components (HOC)
Let’s create a HOC — withSomeData
— that accepts a React component as an argument, implements the data logic, and passes someData
, loading
, and error
as props into the wrapped React component.
import React, { useState, useEffect } from 'react'; | |
const withSomeData = Component => { | |
const ComponentWithSomeData = (props) => { | |
// DATA FETCHING/MANAGEMENT FRAMEWORK OR LIBRARY OF YOUR CHOICE | |
return <Component {...props} someData={someData} loading={loading} error={error} />; | |
}; | |
return ComponentWithSomeData; | |
}; | |
const SomeComponent = ({ someData, loading, error }) => ( | |
<React.Fragment> | |
{loading && <div>{'Loading...'}</div>} | |
{!loading && error && <div>{`Error: ${error}`}</div>} | |
{!loading && !error && someData && <div>{/* INSERT SOME AMAZING UI */}</div>} | |
</React.Fragment> | |
); | |
const SomeComponentWithSomeData = withSomeData(SomeComponent); | |
You can replace line 5 in the snippet above with Redux, Apollo GraphQL, or any data fetching/management layer of your choice.
We can now reuse SomeComponent
(UI component) without withSomeData
(HOC). We can also reuse withSomeData
without SomeComponent
.
Today I learned.
Which approach do you prefer and why?
Resources
- Understanding React Render Props and HOC by Aditya Agarwal
- React Hooks: What’s going to happen to render props? by Kent C. Dodds
- Higher-order components vs Render Props by Richard Kotze
Top comments (0)