nicolodavis.com

Making an Imperative API Declarative using React

December 20, 2018

React is a declarative API that takes your data model and converts it into a view. When your data changes, React takes care of updating the relevant parts of the view automatically and efficiently.

Let’s contrast this with an imperative API.

const scene = Scene.create();
const circle = Circle.create();

circle.moveTo(x, y);
scene.add(circle);
scene.render();

The code above creates a scene with a circle inside it. In this API, we tell the program how to do something rather than what to do.

An equivalent declarative style would look something like:

const Component = (props) => (
  <Scene>
    <Circle x={props.x} y={props.y} />
  </Scene>
)

Doesn’t seem like the declarative style is that much superior in this case, but the difference becomes more evident when you need to change x or y.

In the imperative style, you’ll need to issue another:

circle.moveTo(newX, newY);

In the declarative style you do nothing! The circle is automatically moved whenever x or y changes.

This is really great. However, sometimes you need to use these two styles together. Let’s say you only have the imperative API above available and you want to use it with code that’s written in React.

I’ve found the following formula useful to create wrappers around imperative API’s:

class MyComponent extends React.Component {
  constructor() {
    // create stuff
  }

  componentDidMount() {
    // add stuff (if necessary)
  }

  render() {
    // modify stuff
  }

  componentWillUnmount() {
    // remove stuff (if necessary)
  }
}

The Scene and Circle objects get translated to the following. The Context API is used to make the scene object available inside its children.

import React from 'react';
import { Scene as IScene, Circle as ICircle } from 'imperative';

const SceneContext = React.createContext();

export class Scene extends React.Component {
  constructor() {
    this.scene = IScene.create();
  }

  componentDidMount() {
    this.scene.render();
  }

  render() {
    return (
      <SceneContext.Provider value={this.scene}>
        {this.props.children}
      </SceneContext.Provider>
    );
  }
}

export const Circle = props => (
  <SceneContext.Consumer>
    {context => <CircleImpl {...props} context={context} />
  </SceneContext.Consumer>
);

class CircleImpl extends React.Component {
  constructor() {
    this.circle = ICircle.create();
  }

  componentDidMount() {
    this.props.context.scene.add(this.circle);
  }

  render() {
    this.circle.moveTo(this.props.x, this.props.y);
    return null;
  }

  componentWillUnmount() {
    this.props.context.scene.remove(this.circle);
  }
}

Not only have we made an imperative API play nicely with React, but we also get React’s diffing mechanism in the imperative API for free!

What I mean by this is that in case we’re translating a model like this:

{
  circle1: { x: 1, y: 2 },
  circle2: { x: 2, y: 4 },
}

into a scene with two circles, the imperative API will likely have to clear and re-render the scene when the model changes (unless we cache the old model and compute a diff of what changed).

When we wrap the circles inside React components, they are implictly added to a virtual DOM that React uses internally to figure out what changed before applying an update.

So, if we only change the location of the second circle, React will make sure that the render function of that circle is invoked without touching the first.


Nicolo John Davis
Nicolo Davis