Krzysztof Żuraw blog

On React Render props vs HOC

March 18, 2019

Recently one of my colleague at work stumbled upon strange case when using one of the popular react pattern called hoc (high order component). It turns out that sometimes your hoc can override your passed props. Let me explain it a little bit more.

Imagine that you have a Button component:

import * as React from 'react';

interface OwnProps {
  onClick: () => void;
  amount: number;
  name: string;
}

const Button: React.FunctionComponent<OwnProps> = props => (
  <button onClick={props.onClick}>
    {props.name} {props.amount}
  </button>
);

It’s is a small component that takes 3 props. One onClick to handle button clicks and the rest two are just for displaying data on the button.

How you can pass props to this button? Let’s agree that you need a higher level of abstraction 😂 and you pass them via HOC:

import * as React from 'react';

export interface HOCProps {
  onClick: () => void;
}

export const withHOC = <WrappedComponentProps extends object>(
  WrappedComponent: React.ComponentType<WrappedComponentProps>
) => {
  // tslint:disable-next-line:no-console
  const onClick = () => console.log('Clicked! from HOC');

  const HOC: React.FunctionComponent<WrappedComponentProps & HOCProps> = props => {
    return <WrappedComponent {...props} onClick={onClick} />;
  };
  HOC.displayName = `withHOC(${WrappedComponent})`;
  return HOC;
};

so this can be used like this:

import * as React from 'react';

import { HOCProps, withHOC } from '../hoc';
import { Button } from './button';

interface OwnProps {
  onClick: () => void;
  amount: number;
  name: string;
}

const ButtonHOC = withHOC(Button);

// usage
<ButtonHOC onClick={() => setAmount(amount + 1)} amount={amount} name="HOC button" />;

You clap yourself on the back for such a good work - 👏🏻. You abstract away onClick.

The code looks fine but it turns out that clicking on button results in console.log! But you wanted it to increment amount by one. What is happening?

Your HOC is overriding your component props. To avoid that you will need to change the prop name - so the clashing won’t occur anymore.

Let’s look for another pattern that is common in react world - render props:

import * as React from 'react';

export interface OwnProps {
  render: ({ onClick }: { onClick: () => void }) => JSX.Element;
}

export const RenderPropsButton: React.FunctionComponent<OwnProps> = props => {
  // tslint:disable-next-line:no-console
  const onClick = () => console.log('Clicked from renderProps');
  return props.render({ onClick });
};

it can be used like this:

<RenderPropsButton
  render={renderProps => (
    <Button onClick={renderProps.onClick} amount={amount} name="RenderProps button" />
  )}
/>

Will it help? Yes - as we are passing onClick via renderProp instead of being directly injected into the component. This allows the developer to see from where this prop is coming and fix it.

Summary

When render props first came to react world I did not see a clear benefit over hoc. After seeing a similar case like one above - I find them better in terms of readability than hoc.

PS. I know that those examples are overcomplicated. There are here to prove the point.

TLDR

With render props you can avoid accidental prop override.


Krzysztof Żuraw

Written by Krzysztof Żuraw who lives and works in Wrocław. About page.