Making custom renderers for React

10 Jul 2016

I have been working on a side-project recently. It’s called Pabla (GH), and it is an engaging image creator, a just-for-fun clone of Buffer’s Pablo.

The most interesting thing about it, for me, was exploring ways to do Canvas rendering. I have worked on a sophisticated React+Canvas app in the past, but it was a mess. It had functions which drew almost all of the UI to canvas directly and reacted to events on canvas. In a single huuuge file.

It hurt even navigating in that file, lest changing something or adding features.

So with Pabla, I set to find better ways to make that happen.

If you are looking for a before-after, here you go:

Before — is very imperative and handles a lot of detail.

After — with all these nested components, it looks like any React code you are used to. It also allows you to define custom canvas components, with state and such! The Cursor component neatly encapsulates the blinking.

<Canvas width={canvasWidth} height={canvasHeight}>
  <CanvasImage image={image} frame={mainFrame} />
  <Filter name={filter} frame={mainFrame} />
  <CanvasLine color={color} width={2} from={a} to={b} />
  <CanvasLine color={color} width={2} from={a} to={b} />
</Canvas>

React is great at providing common ground for building UIs, no matter the platform. It provides immense power for rendering to any output, be it native mobile, DOM, canvas, or even the terminal.

In this post, I’m going to share what I learned about the private APIs of React that let it happen. While I will share some thought-process that led me there, some mid-steps will naturally be omitted.

🚧 The APIs discussed in this post are not public, and can/will change in the future.

Skip that if you are in for the reference and not the story.

(I am aware of projects like react-canvas. However, it seemed to have a different use-case in mind. That said, I did learn on some private React APIs from it.)

Declarative first

The goal is to go from imperative to React-style declarative. But do you know what would the first step be?

It’d be to go just declarative first. That is, using data structures to describe what should be drawn onto the canvas, not immediately drawing.

It is going from

drawImage(ctx, img, [0, 0, 300, 300]);
drawRect(ctx, "black", [10, 10, 290, 290]);

to

[
  {
    type: 'image',
    frame: [0, 0, 300, 300],
    image: img
  },
  {
    type: 'rect',
    frame: [10, 10, 290, 290],
    color: "black"
  }
]

plus having a function that can iterate over that array and draw each primitive.

This was a vast improvement already in terms of readability and understanding what’s going on.

It did lack, well, components — the layout had to include all the primitives. It’s true that these could be encapsulated into functions which return parts of this layout, and composing them together, but components are a bit more than on the screen, they’re also about encapsulating particular behavior (like the blinking cursor that I linked).

So what would it take to make it all React-y?

React renderers — high-level intro

It’s not a secret that React can be used to render to anything, even native mobile apps, so it shouldn’t come as a surprise that you can make it render onto canvas.

React is a great generalization of UI, independent of the platform.

Before going into the details, let’s take a higher level look at what we need here, and really, what any rendered consists of.

These are what every renderer has to have to be useful.

With that established, we can build on top of that with React components. Yes, the ones that you already write with class X extends React.Component or React.createClass or functions. They will work with no additional steps required once the baseline renderer is established.

Data structure

Now that that’s clear, let’s add a bit more detail and talk about how we are going to represent that.

It’s evident that the structure we are dealing with here is a tree:

It’s like DOM! And in fact, that’s what React is working on — trees.

Now, the object structure that I’ve shown previously is immutable and can be great when it gets to the actual drawing… But when we need to insert a node here, or remove a node there, we better have a mutable tree (again, like DOM) in place.

For the tree, we are going to need several kinds of nodes:

These nodes are going to map to primitive/group/bridge components 1-to-1.

Concerning operations on nodes, what we are going to need is:

Here’s how I implemented these in Pabla.

The React bridge

On React side, there is also a tree which resembles the tree of your renderer. It’s a tree of “internal instances”.

There are two kinds of these:

An internal instance is an object with the following methods:

Naturally, we are looking to create custom host components for the group and primitive components.

To implement custom child rendering, we are going to need ReactMultiChild. ReactMultiChild is a private mixin that a container component should extend to handle its children in a custom fashion.

Bridge and group are both “containers”, i.e. components that handle child rendering.

Group and primitive are both “nodes”, i.e. things that are drawn.

It makes sense to extract that shared code into ContainerMixin and NodeMixin respectively.

Container

A few important methods that a container has to define are:

And there’s also some boilerplate that needs to be there:

Node

For a group and primitive components, which are going to be custom class components, we are going to store several properties:

And have these methods:


Bridge

The bridge will be using componentDidMount and componentDidUpdate to create/update the backing node, and construct children nodes.

Group

On group specifically, mountComponent and receiveComponent are needed to create/update the backing node, and construct children nodes.

Implementation

The post is not going to contain huge chunks of code.

You can use this file from Pabla as a reference for the full implementation.

Possible use cases

One other possible use case that hit me recently (and in a way, led to reflect on my canvas experience) is making a bridge from React Native to React, which will create a web view and render its children to it:

<ReactWebBridge>
  <h2>Hey!</h2>
</ReactWebBridge>

It’s quite possible in theory, but I’m not sure how’s that going to work out in practice.

(Thanks to Dan Abramov and Andrzej Krzywda for reviewing my drafts.)

Discuss this post on HN.

Want to level up your React skills?

Sign up below and I'll send you content just like this about React straight to your inbox every week.

No spam, promise. I hate it as much as you do!

, enjoying the article? Now think of 3 friends who are interested in React, Canvas and would be into it, and share the link with them! 👇

http://goshakkk.name/react-custom-renderers/