Hackernoon logoExploring Render Props Vs. React Hooks In 2020 by@moubi

Exploring Render Props Vs. React Hooks In 2020

Author profile picture

@moubiMiroslav Nikolov

Reactifying one.com apps. Javascript tips and how-tos at webup.org/blog. Maintaining OSS on github.

Who said render props are obsolete? The good old pattern is still valid for the common use case where hooks may not always be the right choice.
With that said, can you guess which code snippet is more efficient and why?
I started a twitter discussion with the same question recently, trying to understand if people have strong opinion about hooks and render props. Opposing the two is not a fantasy, but comes from a practical concern.
When it comes to state management render prop component:
  • is often more flexible and less error-prone than pure hooks solution.
  • is still suitable for the common case.
In reality, hooks and render props shake hands and play well together. But if you must decide between either of the two, though, let’s put that decision on stress.
Want to see the end benchmark? Skip to the comparison, otherwise read on…
If your are not familiar with hooks and the render props pattern - don’t worry - a good starting point is Render PropsUse a Render Prop! and Hooks at a Glance. A list of resources is also available at the end.

Render Props are Not Dead

A talk with that name by Erik Rasmussen was the trigger for this writing. It outlines how we got from HoCs to hooks. Watch it, it should make things clearer.
I remember the voice in my head when hit the play button on that React Europe video: “Wait, should I do another rewrite of my library, getting rid of the render props I so much like”. At that time v2 of Enform was released and I was happy with it. An immediate v3 rewrite would destroy my positive feeling.
May be you:
  • work with hooks, but may not fully understand them
  • see hooks as a magic solution
  • want to rewrite it all with hooks
If so, then what follows may be a surprise.

The problem

Hooks and render props can solve the same problem. It is conceptually about moving state away from your components, so that it is reusable. The question is which one does better job? When? Does it matter bother with wrapper components and render props since we already have the hooks api?
To answer, let’s work with the common form example below throughout this journey. It’s trivial and you have probably seen it many times:
class Form extends Component {
  constructor(props) {
    super(props);

    this.state = {
      name: props.name || "",
      email: props.email || ""
    }
  }

  render() {
    return (
      <form>
        <input
          value={this.state.name}
          onChange={e => {
            this.setState({ name: e.target.value });
          }}
        />
        <input
          value={this.state.email}
          onChange={e => {
            this.setState({ email: e.target.value });
          }}
        />
      </form>
    );
  }
}
  • The form is intentionally kept simpler.
The snippet may force you think: “This is a recipe for disaster”. Right, and state is the primary suspect. Adding to that, usually you have more fields involved in the form and need to handle validation, submission, API calls, error messages too. Of course, as a result your component will grow and you may need to relief the state logic by abstracting it somehow.

Handling state abstraction with hooks

Look at this simplified code:
function Form() {
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");

  return (
    <>
      <h1>This is a simple form!</h1>
      <form>
        <input
          value={name}
          onChange={e => {
            setName(e.target.value);
          }}
        />
        <input
          value={email}
          onChange={e => {
            setEmail(e.target.value);
          }}
        />
      </form>
    </>
  );
}
It is the same form component, but using a function instead of a class and the 
useState
 hook. Simple move that already made things nicer. Including more fields to this controlled form is as easy as handling more state in the component.
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [phone, setPhone] = useState("");
const [address, setAddress] = useState("");
...
Using hooks and functional components is already a win. OK, but you bump into another trouble - component state is growing together with the form. From that point there are two options. Create separate form component or a custom hook to hold the state heavy lifting.

Form custom hook

I assume you know how to build one. There are many examples out there, so let’s not focus on the useForm implementation below. What is interesting is how it improves our component and how it gets consumed. Remember we are slowly getting to the pain point - would custom hook be the best approach here.
Lastly, please excuse once again the simplicity as the idea is just to illustrate the pattern.
function Form() {
  const { values, setValue } = useForm();

  return (
    <>
      <h1>This is a simple form!</h1>
      <form>
        <input
          value={values.name}
          onChange={e => {
            setValue("name", e.target.value);
          }}
        />
        <input
          value={values.email}
          onChange={e => {
            setValue("email", e.target.value);
          }}
        />
      </form>
    </>
  );
}
Ideally adding more logic would result in just the 
jsx
 (the render) part growing, while 
useForm
 manages the state for you.
Side note: 
useForm()
 (it’s a pretty common name) may miss-reference you to react-hook-form. The name matches, but the idea is different. 
react-hook-form
is not solving the state problem described here, but avoiding it by having the form as uncontrolled instead.
Getting back to our example. Adding errors and submit features:
function Form() {
  const { values, setValue, errors, submit } = useForm();

  return (
    <>
      <h1>This is a simple form!</h1>
      <form onSubmit={submit}>
        <input
          value={values.name}
          onChange={e => {
            setValue("name", e.target.value);
          }}
        />
        <input
          value={values.email}
          onChange={e => {
            setValue("email", e.target.value);
          }}
        />
        <input
          value={values.phone}
          onChange={e => {
            setValue("phone", e.target.value);
          }}
        />
        <p>{errors.phone}</p>
      </form>
    </>
  );
}
Still, it scales pretty good. You can move more logic into the hook and make it reusable for all form components in your project.
The state no longer resides in 
<Form />
, but the component will continue to react on field changes. At the end, it is the same 
useState
 usage, but moved in 
useForm
.
The obvious benefits of this approach are that it’s intuitive (no weird syntax), scales pretty well and it’s probably part of the React future.
Ok, but how about render props?

Handling state with render prop component

Unloading the Form component state-wise using the render props approach requires you to create a wrapper component. So, no hooks on the surface, but a regular component. In this example it is children that serves as a render prop, but you may use render (or something else) instead.
function Form() {
  return (
    <>
      <h1>This is a simple form!</h1>
      <FormManager>
        {({ values, setValue }) => (
          <form>
            <input
              value={values.name}
              onChange={e => {
                setValue("name", e.target.value);
              }}
            />
            <input
              value={values.email}
              onChange={e => {
                setValue("email", e.target.value);
              }}
            />
          </form>
        )}
      </FormManager>
    </>
  );
}
  • Curious about FormManager's implementation? Here is the codesandbox.
Abstracting the state away in a weird way, right? Yes, this is how it is.
From the official docs:
The term “render prop” refers to a technique for sharing code between React components using a prop whose value is a function.
“…using a prop whose value is a function” - exactly what seems awkward when you see render props for first time. Other than that it works similar to 
useForm
except 
<FormManager />
 is just a normal component. This pattern might be familiar, especially if you are working on third party libraries or using such.
The render props approach has similar benefits to hooks, but looks strange and sometimes doesn’t scale efficiently. Why is that?
Imagine the following:
function MyComponent() {
  return (
    <Swipeable
      onSwipeLeft={handleSwipeLeft}
      onSwipeRight={handleSwipeRight}
    >
      {innerRef => (
        <div ref={innerRef}>
          <DragDropContext onDragEnd={handleDragEnd}>
            {() => (
              <Droppable>
                {() => (
                  <Draggable>
                    {provided => (
                      <div
                        ref={provided.innerRef}
                        {...provided}
                      />
                    )}
                  </Draggable>
                )}
              </Droppable>
            )}
          </DragDropContext>
        </div>
      )}
    </Swipeable>
  );
}
Nested wrapper components with render props. Oh, that doesn’t look very promising. It may even trick some people to believe the pattern is obsolete in favor of “do everything with hooks”. Hooks doesn’t suffer the nesting issue, that’s true.
But if render props had no pros over hooks the article is leading to a dead end. It’s not, I promise. There is something else, though, which is not about the syntax.
Keep on…

Reality check

Let’s recap. Remember this part from the beginning?
<>
  <h1>This is a simple form!</h1>
  <form>
    ...
  </form>
</>
I intentionally left more elements (
<h1 />
) than just the 
<form />
 in the jsx. It is suppose to serve as a hint, because in reality some components aren’t that simple. Often they render others or third party ones which you don’t have control over.
A more realistic example would look like so:
function Page() {
  const { values, setValue } = useForm();

  return (
    <>
      <Header />
      <Navigation />
      <SomeOtherThirdPartyComponent />
      <form>
        <input
          value={values.name}
          onChange={e => {
            setValue("name", e.target.value);
          }}
        />
        <input
          value={values.email}
          onChange={e => {
            setValue("email", e.target.value);
          }}
        />
      </form>
      <Footer />
    </>
  );
}
Now, I know you may say: who uses jsx like that? You can obviously extract the form logic into another component and render it here instead. Yes and you would be right - seems the correct thing to do, but not always.
There are three general restrictions with hooks:
  • you need react@16.8.0 (the one with hooks)
  • you have to use functional components
  • you may fall into re-render issues
Skipping the first two… If you have class components and lower version of react you can’t use hooks obviously. The third one, however, is the cornerstone when deciding between hooks and render props.

You may fall into re-render issues

Given the last example, every time you type in the form fields 
setValue
will be called causing the whole 
<Page />
 component to re-render. And because you are updating the state, this is expected. But not desirable. Suddenly filling a form may become very expensive operation.
React is clever enough to protect you from unnecessary renders, but it won’t go against its principles. Every component has its own catch-ups and you need to work around these, so it’s safe against renders. Unfortunately, it may not be the case with 
<Header />
<Navigation />
 and 
<Footer />
 because, let’s imagine, you don’t have time to refactor them. And with 
<SomeOtherThirdPartyComponent />
 you may even not be able to do so.
Not many options here. Extracting the from in a separate component is the way to go with hooks. As a consequence - you will need to repeat that for every form in your project making the tree grow inevitably.
What if you are building a form library that exports a hook like 
useForm
? Do you prefer your users to do the extra extraction step above? Not a big deal you may say. Not a big one, but a less flexible one.
Hooks are not remedy for all problems and they are not intended to serve that purpose. The hypothetic (or not) primer above is one of these cases where you may need the extra flexibility.
Stick to hooks, but add some sugar.

Re-render only what is relevant

Render props doesn’t suffer the same re-render issue hooks do. Here is why.
function Page() {
  return (
    <>
      <Header />
      <Navigation />
      <SomeOtherThirdPartyComponent />
      <FormManager>
        {({ values, setValue }) => (
          <form>
            <input
              value={values.name}
              onChange={e => {
                setValue("name", e.target.value);
              }}
            />
            <input
              value={values.email}
              onChange={e => {
                setValue("email", e.target.value);
              }}
            />
          </form>
        )}
      </FormManager>
      <Footer />
    </>
  );
}
<FormManager />
 ensures whatever change is made in the form it will be isolated in that form. 
<Page />
 is immune to unnecessary renders. You can add up more jsx with no side effects.
Of course you can always break the useful pattern. Imagine updating some 
<Page />
 related state as a result of form manipulation. It will result in additional renders. But then, it won’t be FormManager’s fault.
Now if your form library exports component with a render prop instead, its users get that extra flexibility. They are no longer forced to create additional components.

Comparison in action

Putting these two implementations side by side:
Voilà. You can now see the render outcome of each form. The one on the left (custom hook) is causing re-renders in all Page children, while the one on the right (render prop) doesn’t.

Final words

Render props are very useful if you want to isolate part of the jsx and inject some state without introducing side effects to your components.
It is very common that many render prop implementations are using hooks internally so saying “it’s hooks or nothing” would be extreme. Hooks support the pattern pretty well and gain the extra flexibility they lack in some situations. This is to consider when deciding between one OR the other approach.
But hey, your form library can also export both the wrapper component and the hook. This too is very common. That makes working on open source so fun.
If you find this post useful 👍, join the newsletter for more interesting biweekly content. That way I can easily deliver 📩 to you next time. Follow me on twitter for related discussions.

Resources

The list here is not extensive. Some of the topics are more advanced, some are touching just the basics. You are welcome to add to it.
  1. Render Props (React docs)
  2. Hooks at a Glance (React docs)
  3. Use a Render Prop! (Michael Jackson)
  4. Awesome React Render Props (Jared Palmer)
  5. Render Props are Not Dead (Erik Rasmussen)
Credits: Photo by Austin Neill on Unsplash.

Tags

The Noonification banner

Subscribe to get your daily round-up of top tech stories!