React Render Props in TypeScript

Update

In React v16.8.0, React Hooks were released, a first-class solution that covers reusing logic across multiple components and that can be used to replace the uses of render props as described in this article. For more information on using Hooks with Typescript, see React Hooks in TypeScript.


As with the previous article, this article assumes knowledge of the subject in question, so for some background on render props, the official documentation can help. This article will use the function-as-a-child render prop pattern to match its usage within React itself for the new context API. If you prefer using another prop, such as render, just substitute children for your prop in the coming examples.


To demonstrate the render prop pattern, we will be converting the makeCounter HOC from the previous article to a render prop component. To recap, here is the HOC version:

The HOC injects the value and two callback functions (onIncrement and onDecrement) to a component, in addition to enhancing the component with a minValue and maxValue prop, which the HOC uses internally and does not pass this through to the component. We discussed how not passing through props may present an issue if the component needs to know these values, and that the naming of the injected value prop may also conflict with props injected by other HOCs if wrapping a component with multiple HOCs.

The makeCounter HOC can be rewritten as a render prop component like so:

There are a few changes here to take note of. Firstly, InjectedCounterProps is retained, as we want an interface to define the props that the render prop function will be called with rather than passed through to the component (as with the HOC). The props of MakeCounter (MakeCounterProps) have changed though, with this addition:

children(props: InjectedCounterProps): JSX.Element;

This is the render prop itself, and states that the component requires a function passed in that takes in the injected props and returns a JSX element. Here is an example of its usage to highlight this:

MakeCounter‘s own component declaration then becomes much simpler; it is no longer wrapped in a function as it is no longer a HOC, and the typing is much more straightforward, with no mind-bending generics, subtraction and type intersection. It is simply MakeCounterProps and MakeCounterState, like any other component:

class MakeCounter extends React.Component<
MakeCounterProps,
MakeCounterState
>

Finally, render() also becomes less work too; it’s just a function call with the injected props — no destructuring and object rest/spread needed!

return this.props.children({
value: this.state.value,
onIncrement: this.increment,
onDecrement: this.decrement,
});

The render prop component then allows for more control over the naming of the props and flexibility in their usage, which was an issue with the equivalent HOC:


With all of these benefits, in particular the much simpler typing, then why not use render props all of the time over HOCs? You certainly could, and there would be no issues doing so, but there are a few issues with render prop components to be aware of.

Firstly, there is an issue with separation of concerns; MakeCounter is now baked into the Counter component rather than wrapping it, making it more difficult to test the two in isolation. Secondly, as the props are injected in the render function of component, you cannot make use of them in the lifecycle methods (provided Counter was changed to a class component).

Both of these issues are easy to solve, as you can simply make a new component using the render prop component:

Another issue is that it could be seen as less convenient in general — it is now a lot of boilerplate that the consumer has to write, especially if they only want to wrap their component in a single HOC and use the props as-is. This can be remedied by generating a HOC from the render prop component:

Here, the techniques of the previous blog post, along with the existing types of the render prop component, are being used to generate the HOC. The only thing to take note of here is that we must remove the render prop (children) from the HOC’s props so it is not exposed when used:

type MakeCounterHocProps = Omit<MakeCounterProps, 'children'>;

In the end, the tradeoff between HOCs and render prop components comes down to flexibility vs convenience. This can be solved by writing render prop components first, then generating HOCs from them, which gives the consumer the power to choose between the two. This approach is becoming common amongst reusable component libraries, for example the excellent render-fns library.

As far as TypeScript goes, there is no doubt that typing HOCs is much more difficult; though with the examples in these two posts, it shows that this burden is on the provider of the HOC and not the consumer. In terms of usage it could be argued that consuming a HOC is less difficult than consuming a render prop component.

Prior to React v16.8.0, I’d recommend sticking with render prop components for the flexibility and simplicity of typing, and if the need arises, such as building a reusable library of components, or for render prop components that are simple and/or widely used across a project, I would only then generate a HOC from them. Following the release of React Hooks in React v16.8.0, I would strongly recommend using them over both higher-order components or render props where possible, as they are much simpler to type.