I'm Learning About: React Component Communication

April2019

It's been over a year since I wrote my last blog post. I kept meaning to keep blogging, I just never got around to it. That's how these things go, I guess.

One of the things I had always imagined doing was a series called I'm Learning About, as a contrast to the popular Today I Learned. The idea is that I will just regularly blog about whatever I'm learning about as I go about my work, particularly on side projects. I like this framing more than TIL because a) I'm a slow learner and it often takes me more than a day to learn things, and b) learning is a process, not an event. As an engineer, I'm engaged in a process of continual learning, and I want my blog to support that.

With that preamble out of the way, let's get down to what I've actually been learning. I'm fortunate enough to have regular time to work on side projects, and lately I've been rewriting my graph project  to use React. The original project is a javascript library and UI for generating graphs (as in graph theory), and it's actually pretty good. It works as I want it to, and was good enough to create my last blog post.

But I always knew it was a prototype. I tried to avoid getting too involved in actually rendering the svg, and used libraries that I didn't fully understand. So my first task has been to rewrite in react, so I can have more control over what's rendered and provide more interactivity. My goal is to be able to render any react component inside a graph, and to have fine grained control over the graph's behavior.

Now I have rebuilt it up to the point where I can write some react code and have it render a graph:

<Graph width={250} height={250}>
  <Node nodeId="red-node-1">
    <Circle color="#fc2f38" />
  </Node>
  <Node nodeId="red-node-2">
    <Circle color="#fc2f38" />
  </Node>
  <Node nodeId="blue-node-1">
    <Circle color="#5b41fc" />
  </Node>
  <Node nodeId="blue-node-2">
    <Circle color="#5b41fc" />
  </Node>
  <Node nodeId="yellow-node">
    <Circle color="#fcf95d" />
  </Node>
  <Node nodeId="green-node">
    <Circle color="#3ba226" />
  </Node>
  <Edge fromNodeId="red-node-1" toNodeId="yellow-node" />
  <Edge fromNodeId="red-node-1" toNodeId="blue-node-1" />
  <Edge fromNodeId="red-node-1" toNodeId="green-node" />
  <Edge fromNodeId="red-node-2" toNodeId="yellow-node" />
  <Edge fromNodeId="red-node-2" toNodeId="blue-node-1" />
  <Edge fromNodeId="red-node-2" toNodeId="blue-node-2" />
  <Edge fromNodeId="red-node-2" toNodeId="green-node" />
  <Edge fromNodeId="blue-node-1" toNodeId="green-node" />
  <Edge fromNodeId="blue-node-1" toNodeId="yellow-node" />
  <Edge fromNodeId="blue-node-2" toNodeId="green-node" />
</Graph>
,

You'll notice my design goals already start to show up in how the components are used. Since I want to be able to put anything inside a Node, I've written it so that Node just accepts some children and renders them at the appropriate place in the graph.

This gets at the fundamental reason I like React for this project. React makes composition easy. It's very natural in React to define new components in terms of old ones; for instance, I can take all the nodes with circles inside them above and define a new component to make that easy:

<Node nodeId="red-node-1">
  <Circle color="#fc2f38" />
</Node>

<CircleNode nodeId="red-node-1" color="#fc2f38" />

Obviously that's a very simple transformation, but component composition lets us build up more and more complicated patterns. For instance, we can start with anImageNode to render images inside of a node, then add a layer on top to render Magic: The Gathering cards by name and graph them by synergy:

<Graph width={500} height={500}>
  <Card card="Galloping Lizrog" size={0.75} />
  <Card card="Aeromunculus" size={0.75} />
  <Card card="Skatewing Spy" size={0.75} />
  <Card card="Gruul Beastmaster" size={0.75} />
  <SynergyEdge from="Galloping Lizrog" to="Aeromunculus" />
  <SynergyEdge from="Galloping Lizrog" to="Skatewing Spy" strength={3} />
  <SynergyEdge from="Galloping Lizrog" to="Gruul Beastmaster" />
  <SynergyEdge from="Skatewing Spy" to="Gruul Beastmaster" strength={1.2} />
</Graph>
,

This is what I like about React development. React encourages you to develop a vocabulary of components that you combine to create the desired page.

Design Goals and Component Communication

There is something going on under the hood that led to the problem I'm writing about today. The Graph component uses a d3 force simulation  to control the layout. We need to make d3 api calls to add nodes and edges to the simulation. This presents an interesting conflict with one of my design goals - that the components be self-representative. That is, I want it to be the case the the component tree as written down configures the simulation. There should be no extra configuration for the Graph besides inserting Nodes and Edges as children. I won't get into why this is important to me right now, but if you're curious you can send me an email.

First Attempt: using refs

At first I was thinking about this problem using a traditional object-oriented frame. "The graph needs to read some configuration from its children," I thought, "and then use that to construct the simulation." That feels like a natural way to frame the question when you are used to constructing objects and making them interact. This would be pretty simple from an object-oriented point of view - make the children implement some interface, pass them to the graph, and have the graph pull from its children to decide what methods to call on the simulation.

I looked around to figure out how to allow parent components to call methods on their children and I stumbled upon refs. Refs were exactly what I wanted. I could just make the children provide some method (using a convention because javascript doesn't have interfaces), have the graph pass a ref to its children, and then call the agreed-upon method. I named the methodgetSimulationConfig:

getSimulationConfig() {
  const elementShapes = {};
  elementShapes[this.props.nodeId] = this.getShape();
  return new SimulationConfig({
    elementIds: [ this.props.nodeId ],
    elementShapes,
    constraints: [
      new PreventCollisionsConstraintDefinition({
        elementId: this.props.nodeId
      }),
    ]
  });
}

The first problems with this approach came up because of shapes. You can see the elementShapes above; that's another interesting problem, but for now I'll just say that determining the element shape is necessary to make the connections in the graph only go to the edge of the node. At first the only shapes I had were circles, and there wasn't a problem. Then I added labels, and I wanted the shape to be a rectangle whose size was determined by the size of the text. That meant my getSimulationConfig method had to get information from the dom, and it wasn't certain when that would come back. It needed to return a promise.

That was starting to cross the threshold of my tolerance for complexity, but the real kicker was how much more difficult the ref solution made writing wrappers for nodes. Each wrapper around a node would have to implement getSimulationConfig, pass a ref to its node children, and return the result. That was way more hassle than I wanted, especially when you start thinking about wrappers that render multiple nodes.

Better Solution: use Context

The whole time I was using refs I kept thinking that maybe a Redux-style solution would be better. Maybe, I thought, the nodes should register themselves with the graph simulation. If there was some object that lived outside the component tree they could use to register, that might make the design much simpler

I had heard some things about Context.  After reading up on it, I was delighted with how simple it was. Basically, you just render aContext.Provider, and any components nested underneath can subscribe to it by declaring a static property contextType. When you render your context provider, you assign a value to it, and the value can be anything you want, even an object with methods. This is what I ended up with:

export default React.createContext({
  registerElement: (elementId, shape) => {},
  getElement: elementId => {},
  registerForce: force => {},
  registerConstraint: constraint => {},
});

Now nodes pass along a callback to their children that registers the element when the shape is available.

At first I was dissapointed that I had to rewrite my code, and I was worried that this would put a lot of complexity on the simulation class. Now the simulation config wouldn't be specified up front, elements and forces would come in whenever they were ready. I thought that might be hard. But it turned out to dramatically simplify my code. d3 is written to handle changes to the simulation pretty easily, and the simplication to my graph components is very nice.

Anyways, thanks for reading this far! If you did, you might want to subscribe to my mailing list or rss feed. The code I talked about in this post is available on github.