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
displayNameon the HOC is not necessary, but it is very helpful for debugging your app inreact-devtools - Usually I spread the original
propslast - 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 urls dynamically either based on some ids 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
ArticlePageexample above, the component will receivedataanderrorin 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)