If you've used hooks at all, you'll know that one limitation is not being able to conditionally call them in the render body of your component. So what can we do if we really need to call a hook conditionally? This article demonstrates one possible option—using a generic component with a render prop.
The code speaks for itself, so let's dive straight in. I've included some notes beneath the code examples, as well as an explanation of why this does not break the rules of hooks.
Here is a simple example where a component conditionally calls the
useState hook. The condition is initially
false and then set to
true after 3 seconds.
Here is a more complicated example that renders a list, where each list item has its own state via the
useState hook. The size of the list may change, but the items which remain in the list will always preserve their state.
Wait, doesn't this break the rules of hooks?
Not as far as I'm aware.
The rules say not to call hooks inside nested functions. It looks like we're breaking that rule here, but we're not:
- We're calling a hook at the top level of a regular function.
- We pass that function to a component as a render prop.
- That component calls the render prop function at the top level.
The rules also say not to call hooks inside regular functions (i.e. a function that isn't a hook or a function component), but again—if we look at when that function is actually called (at the top level of
RenderFunction), we can see that it is no different to calling a hook directly inside of a React function component.
Another way to think of this is that the render prop is no different from a custom hook:
- We can call hooks inside custom hooks.
- A custom hook is, by definition, a regular function which must be called at the top level of a React function.
- Our render prop is a regular function called at the top level of a component, so it meets the criteria for a custom hook.
- Therefore you can think of the render prop as a custom hook (one that returns an element).
The wording in these rules is subject to interpretation, but the rules exist to ensure that hooks behave as they should (i.e. hooks are called in the same order each render). The usage of hooks presented here is perfectly safe in this regard, as demonstrated by the examples above. That is surely proof it does not break the rules!
Are there any downsides?
Yes. The lint rules provided by
eslint-plugin-react-hooks may produce false positives (i.e. it returns errors when the code is actually fine), but I'm hoping we can get that fixed. The more people that want this, the more likely we are to get it!
In the meantime, this might be a good reason to avoid using this approach. We've started using this approach in production at Unsplash, but your mileage may vary!
A similar idea was mentioned in this article: if we can't conditionally call a hook, we can wrap the hook in a component and call that conditionally instead.
The approach I'm suggesting here just takes this to its logical conclusion. Instead of creating a static component each time we need to wrap a hook, we can create one component and dynamically provide a render prop which calls the hook. One advantage of this is that our render prop has access to the parent component's closure, whereas if the hook lived inside another component, we may need to drill some values down to the child component (which contains the hook) as props.