July 17, 2020

Calling React Hooks Conditionally/Dynamically Using Render Props

Oliver Joseph Ash
Oliver Joseph Ash

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.

import React from "react";
import { render } from "react-dom";

const RenderFunction = ({ children }) => children();

const MyComponent = ({ shouldRenderHook }) =>
  shouldRenderHook ? (
    <RenderFunction>
      {() => {
        const [state, setState] = React.useState(0);
        return (
          <div>
            State: {state}
            <button onClick={() => setState(currentState => currentState + 1)}>
              Increment
            </button>
          </div>
        );
      }}
    </RenderFunction>
  ) : (
    <div>Rendering the hook shortly…</div>
  );

const rootEl = document.getElementById("root");

render(<MyComponent shouldRenderHook={false} />, rootEl);

setTimeout(() => {
  render(<MyComponent shouldRenderHook={true} />, rootEl);
}, 3000);
View on StackBlitz

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.

import React from "react";
import { render } from "react-dom";

const RenderFunction = ({ children }) => children();

const List = ({ items }) => (
  <ul>
    {items.map(item => (
      <RenderFunction key={item}>
        {() => {
          const [state, setState] = React.useState(0);
          return (
            <li>
              State: {state}
              <button
                onClick={() => setState(currentState => currentState + 1)}
              >
                Increment
              </button>
            </li>
          );
        }}
      </RenderFunction>
    ))}
  </ul>
);

const rootEl = document.getElementById("root");

let items = [1, 2, 3];

render(<List items={items} />, rootEl);

setInterval(() => {
  items = [...items, items[items.length - 1] + 1];
  render(<List items={items} />, rootEl);
}, 2000);
View on StackBlitz

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:

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!

Prior art

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.

Share article