With the official release of hooks, everybody seems to be writing function components exclusively, some even started refactoring all of their old class components. However class components are here to stay. We can't use hooks everywhere (yet), but there are some easy solutions.
Higher Order Components
Higher Order Components (or HOCs) are functions that takes a Component
in its arguments, and returns a Component
. Before hooks, HOCs are often used to extract common logic from the app.
A simple HOC with a useState
hook looks like this:
const withFoo = (Component) => {
function WithFoo(props) {
const [foo, setFoo] = useState(null);
return <Component foo={foo} setFoo={setFoo} {...props} />
}
WithFoo.displayName = `withFoo(${Component.displayName})`;
return WithFoo;
};
Here, our withFoo
function, can be called with a Component
. Then, it returns a new Component
that receives an additional prop foo
. The WithFoo
(note the capitalized With
) is actually a function component - that's why we can use Hooks!
A few quick notes before we move on:
- Personally I usually name my HOCs
with*
, just like we always use the patternuse*
for hooks. - Setting a
displayName
on the HOC is not necessary, but it is very helpful for debugging your app inreact-devtools
- Usually I spread the original
props
last - this avoids overwriting props provided by the users of the component, while allowing the users to override the new fields easily.
Our Custom Hook
How do apply this to our useGet
hook?
Let's replace useState
from the example above to useGet
... but wait, useGet
needs to be called with { url }
- where do we get that? 🤔
For now let's assume the url
is provided to the component in its props:
const withGetRequest = (Component) => {
function WithGetRequest(props) {
const state = useGet({ url: props.url });
return <Component {...state} {...props} />
}
WithGetRequest.displayName = `withGetRequest(${Component.displayName})`;
return WithGetRequest;
};
This works, but at the same time, this means whoever uses our wrapped component will have to provide a valid url
in its props. This is probably not ideal because often we build url
s dynamically either based on some id
s or in some cases, user inputs (e.g. In a Search
component, we are probably going to take some fields from the component's state
.)
One of the limitations of HOCs is they are often "static": meaning we can't change its behavior easily at run-time. Sometimes we can mitigate that by building "Higher Higher Order Components" (not an official name) like the connect
function provided by react-redux
:
// connect() returns a HOC
const withConnectedProps = connect(mapStateToProps, mapDispatchToProps);
// we use that HOC to wrap our component
const ConnectedFoo = withConnectedProps(Foo);
So, if our resource's url relies on some fields from the from the props maybe we can build something like this:
// first we take a function that will be called to build a `url` from `props`
const makeWithGetRequest = (urlBuilder) => {
return withGetRequest = (Component) => {
return function WithGetRequest(props) {
const url = urlBuilder(props);
const state = useGet({ url });
return <Component {...state} {...props} />;
}
};
};
It's safe to assume that different components will have different logic for building the URLs they need. For example, to wrap an ArticlePage
component:
// we know articleId and categoryId will be provided to the component
const buildArticleUrl = ({ articleId, categoryId }) => {
return `/categories/${categoryId}/articles/${articleId}`;
};
// now our enhanced component is using the `useGet` hook!
export default makeWithGetRequest(buildArticleUrl)(ArticlePage);
This seems nice, but it doesn't solve the problem of building url
with the component's state
. I think we are too fixated on this HOC idea. And when we examine it closely we will discover another flaws with this approach - we are relying on props with fixed names being provided to the component, this could lead to a couple of problems:
- Name Collision: Users of the enhanced component will have to be extra careful to not accidentally override props provided by HOCs
-
Clarity: Sometimes the prop names are not descriptive. In our
ArticlePage
example above, the component will receivedata
anderror
in its props and it could be confusing to future maintainers. - Maintainability: When we compose multiple HOCs, it becomes harder and harder to tell which props must be provided by the user? which props are from HOCs? which HOC?
Let's try something else.
Render Props / Function as Child
Render Props and Function as Child are both very common react patterns and they are very similar to each other.
Render Props is a pattern where a component takes a function in its props, and calls that function as the result of its render
(or conditionally, in advanced use cases).
An example with hooks looks like this:
const Foo = ({ renderFoo }) => {
const [foo, setFoo] = useState(null);
return renderFoo({ foo, setFoo });
};
// to use it:
class Bar extends Component {
// ...
render () {
return (
<Foo
renderFoo={
({ foo, setFoo }) => {
// we have access to the foo state here!
};
}
/>
);
};
};
When we decide that the user should always provide that render function as children
, then we are using the "Function as Child" pattern. Replacing renderFoo
with children
in our example above will allow us to use it this way:
<Foo>
{
({ foo, setFoo }) => {
// now we can use foo state here
}
}
</Foo>
The two patterns here are often interchangeable - many devs prefer one over the other, and you can even use them at the same time to provide max flexibility, but that'll be a topic for another time.
Let's try this pattern with our useGet
hook.
// it takes two props: url and children, both are required.
const GetURL = ({ url, children }) => {
const state = useGet({ url });
return children(state); // children must be a function.
};
// now we can use it like this!
class Search extends Component {
// ...
render() {
const { keyword } = this.state;
return (
<GetURL url={buildSearchUrl({ keyword })}>
{
({ isLoading, data, error }) => {
// render the search UI and results here!
}
}
</GetURL>
);
}
}
Easy, right?
Function as Child & Render Props are not without trade-offs. They are more flexible than HOCs but now our original component's JSX is now nested in an inline function - making it a bit tricky to test when using the shallow
renderer from enzyme
. And what happens if we want to compose multiple hooks in a component? I wouldn't nest another function child inside an existing one.
Wrapping Up
Now we have two ways of making hooks (re-)usable everywhere! If a hook doesn't rely on any dynamic inputs, I would go with the HOC solution; If you want to be more flexible, providing a component with Render Props / Function as Child would be a much better choice.
Next let's talk about testing our hooks & components with jest
, sinon
and @testing-library/react-hooks
. 🎉
Top comments (0)