Datavis 2020 Episode 10 – Let’s Make a Face Part VI (Compartmentalizing Complexity)

We’ll discuss

Semantically meaningful JSX refactoring
Using nested React components
How to debug a ReferenceError
The special children prop
How refactoring lets complexity scale
Small multiples with JSX and Array.map
Using randomness to generate graphics

In our previous lesson, we refactored these components so that we have background, circle, eyes, and mouth. I find this to be much more readable than what we had before. We have semantic meaning in these names themselves. The last frontier here is, we’ve got this bit of sort of cryptic code that’s not semantically meaningful.

  <svg width={width} height={height}>
    <g transform={`translate(${centerX},${centerY})`}>

What I would like to do is replace this with something meaningful, like <FaceContainer>.

const App = () => (
<FaceContainer>
      <BackgroundCircle
        radius={centerY - strokeWidth / 2}
        strokeWidth={strokeWidth}
      />
      <Eyes
        eyeOffsetX={eyeOffsetX}
        eyeOffsetY={eyeOffsetY}
        eyeRadius={eyeRadius}
      />
      <Mouth mouthRadius={mouthRadius} mouthWidth={mouthWidth} />
</FaceContainer>
);

Now let’s try making this <FaceContainer> component 

const FaceContainer = () => (
  <svg width={width} height={height}>
    <g transform={`translate(${centerX},${centerY})`}>
    </g>
    </svg>
)

What we want to do is put the stuff that’s nested inside of this FaceContainer in index.js – we need to put that in between these tags here, inside of this group element. 

The special children prop

And how the heck are we supposed to do that? This is where we can leverage a very special prop called children. We can just say {props.children}, and we need to take props as input, and boom! it works! 

const FaceContainer = (props) => (
  <svg width={width} height={height}>
    <g transform={`translate(${centerX},${centerY})`}>{props.children}</g>
  </svg>
);

And again, we can use the same ES6 destructuring syntax as before, to destructure your children like this. 

const FaceContainer = ({ children }) => (
  <svg width={width} height={height}>
    <g transform={`translate(${centerX},${centerY})`}>{children}</g>
  </svg>
);

We can’t make this a module yet, because it refers to width, height, centerX, and centerY. So we could add these as props. 

const FaceContainer = ({ children, width, height, centerX, centerY }) => (
  <svg width={width} height={height}>
    <g transform={`translate(${centerX},${centerY})`}>{children}</g>
  </svg>
);

Now we can pass in these props in the App component. 

const App = () => (
  <FaceContainer
    width={width}
    height={height}
    centerX={centerX}
    centerY={centerY}
  >
    <BackgroundCircle
      radius={centerY - strokeWidth / 2}
      strokeWidth={strokeWidth}
    />
    <Eyes
      eyeOffsetX={eyeOffsetX}
      eyeOffsetY={eyeOffsetY}
      eyeRadius={eyeRadius}
    />
    <Mouth mouthRadius={mouthRadius} mouthWidth={mouthWidth} />
  </FaceContainer>
);

All right! Now we can actually move FaceContainer into a different module. We can say,

export const FaceContainer = ({ children, width, height, centerX, centerY }) => (
  <svg width={width} height={height}>
    <g transform={`translate(${centerX},${centerY})`}>{children}</g>
  </svg>
);

…and cut and paste this logic into a new file – FaceContainer.js.  And then in index.js, import { FaceContainer } from './FaceContainer'

Semantically meaningful JSX refactoring

Now we’re ready to make the ultimate component – namely, the <Face> component. But why would you want to do this? See, conceptually, the <App> component has a bird’s eye view of things, and it’s sort of the orchestrator. Ideally, the <App> code, when you read it, should just be really simple to read. It should just say, ok here’s a face. The <App> shouldn’t really need to know that a face has a background circle, eyes, and a mouth. It should just know that there’s a face. So let’s do this refactoring, where we take all this stuff and we make it its own component, namely the Face component. const Face equals a function that returns the same JSX we had earlier.

const Face = () => (
  <FaceContainer
    width={width}
    height={height}
    centerX={centerX}
    centerY={centerY}
  >
    <BackgroundCircle
      radius={centerY - strokeWidth / 2}
      strokeWidth={strokeWidth}
    />
    <Eyes
      eyeOffsetX={eyeOffsetX}
      eyeOffsetY={eyeOffsetY}
      eyeRadius={eyeRadius}
    />
    <Mouth mouthRadius={mouthRadius} mouthWidth={mouthWidth} />
  </FaceContainer>
);

const App = () => <Face />;

All right it works! Now we need to go through the same exercise as before, where we need to pass in all these things as props – width, height, centerX, centerY, strokeWidth, eyeOffsetX, eyeOffsetY, and eyeRadius. And I’ll just reformat that long line to look like this. 

const Face = ({
  width,
  height,
  centerX,
  centerY,
  strokeWidth,
  eyeOffsetX,
  eyeOffsetY,
  eyeRadius
}) => (
  <FaceContainer
    width={width}
    height={height}
    centerX={centerX}
    centerY={centerY}
  >
    <BackgroundCircle
      radius={centerY - strokeWidth / 2}
      strokeWidth={strokeWidth}
    />
    <Eyes
      eyeOffsetX={eyeOffsetX}
      eyeOffsetY={eyeOffsetY}
      eyeRadius={eyeRadius}
    />
    <Mouth mouthRadius={mouthRadius} mouthWidth={mouthWidth} />
  </FaceContainer>
);

Now we need to pass in all that stuff and that looks awfully similar to all these variables here at the start of index.js. Maybe we can actually get rid of these variables. The only problematic thing is, centerX is defined in terms of width. So I think we want to just leave these two – width and height, and all the rest of this could be computed as the props as they’re being passed in. 

So I’m going to cut this from here and paste it as the props to our Face component. 

const App = () => (
  <Face
    centerX = {width / 2}
    centerY ={height / 2}
    strokeWidth = {20}
    eyeOffsetX = {90}
    eyeOffsetY = {100}
    eyeRadius = {40}
    mouthWidth = {20}
    mouthRadius = {140}
    />;
);

How to debug a ReferenceError

This is not working. Do we have any runtime errors? Oh! looks like I missed a few props – mouthRadius is not there, I’m noticing that width and height are required here, but I don’t think we passed those in. So, let me pass those in too. 

import React from 'react';
import ReactDOM from 'react-dom';
import { BackgroundCircle } from './BackgroundCircle';
import { Eyes } from './Eyes';
import { Mouth } from './Mouth';
import { FaceContainer } from './FaceContainer';

const width = 960;
const height = 500;

const Face = ({
  width,
  height,
  centerX,
  centerY,
  strokeWidth,
  eyeOffsetX,
  eyeOffsetY,
  eyeRadius,
  mouthWidth,
  mouthRadius
}) => (
  <FaceContainer
    width={width}
    height={height}
    centerX={centerX}
    centerY={centerY}
  >
    <BackgroundCircle
      radius={centerY - strokeWidth / 2}
      strokeWidth={strokeWidth}
    />
    <Eyes
      eyeOffsetX={eyeOffsetX}
      eyeOffsetY={eyeOffsetY}
      eyeRadius={eyeRadius}
    />
    <Mouth mouthRadius={mouthRadius} mouthWidth={mouthWidth} />
  </FaceContainer>
);

const App = () => (
  <Face
    width={width}
    height={height}
    centerX={width / 2}
    centerY={height / 2}
    strokeWidth={20}
    eyeOffsetX={90}
    eyeOffsetY={100}
    eyeRadius={40}
    mouthWidth={20}
    mouthRadius={140}
  />
);

const rootElement = document.getElementById('root');
ReactDOM.render(<App />, rootElement);

And boom! it works. All right, now we can actually move our Face component into a separate file. 

Using nested React components

So I’ll make a new file called Face.js, I’ll cut all this code here, and go over to Face.js, and paste it. And then use the export keyword, and then over in index.js, import { Face } from './Face';

It’s not working. But I don’t need to look, because I know that the problem is. All this stuff is not being imported into the Face module. 

import { BackgroundCircle } from './BackgroundCircle';
import { Eyes } from './Eyes';
import { Mouth } from './Mouth';
import { FaceContainer } from './FaceContainer'; 

So I’m gonna cut these imports out of here, and go over to Face.js, and paste them over here. All right, very good. We have completed the refactoring. Now everything is sort of on a need-to-know basis. The App only needs to know about the Face, and what values it needs, and the Face doesn’t need to know anything about the parent component. It only needs to know about its children. The Face needs to know about these components, and the App doesn’t. 

How refactoring lets complexity scale

That’s the beauty of ‘component-izing’ things. And consider the d3-arc import. That’s only needed by Mouth. The only thing that needs to know about arc from d3 at all is just the Mouth component. This kind of refactoring with components and modules allows complexity to scale, because it compartmentalizes the complexity. Having things on a need-to-know basis. It lets us express things in sort of a higher-level language. 

For example, we could even have multiple faces. 

const App = () => (
  <>
  <Face
    width={width}
    height={height}
    centerX={width / 2}
    centerY={height / 2}
    strokeWidth={20}
    eyeOffsetX={90}
    eyeOffsetY={100}
    eyeRadius={40}
    mouthWidth={20}
    mouthRadius={140}
  />
    <Face
    width={width}
    height={height}
    centerX={width / 2}
    centerY={height / 2}
    strokeWidth={20}
    eyeOffsetX={90}
    eyeOffsetY={100}
    eyeRadius={40}
    mouthWidth={20}
    mouthRadius={140}
  />
  </>
);

Now two faces are there, you can’t see them because the width and height need to be adjusted. Let’s adjust the width and height. Let’s say 166. 

const width = 166;
const height = 166;

And we’ve got two faces. But now, everything else is sort of off. So let me just make some adjustments. So I’ll just tweak things until that face looks sort of right. 

const App = () => (
  <>
  <Face
    width={width}
    height={height}
    centerX={width / 2}
    centerY={height / 2}
    strokeWidth={10}
    eyeOffsetX={30}
    eyeOffsetY={30}
    eyeRadius={10}
    mouthWidth={10}
    mouthRadius={40}
  />
    <Face
    width={width}
    height={height}
    centerX={width / 2}
    centerY={height / 2}
    strokeWidth={20}
    eyeOffsetX={90}
    eyeOffsetY={100}
    eyeRadius={40}
    mouthWidth={20}
    mouthRadius={140}
  />
  </>
);

Imagine if we did not have components, or props, or anything like that. Imagine how many different places I would need to change things in order to do that sort of adjustment. 

Small multiples with JSX and Array.map

After you’ve done a refactoring like this you can just change things all in one place we can even make a bunch of faces. We can do that by starting with an array. Let’s say const array = [1, 2, 3, 4, 5], and then once we’ve got this array, we can make one Face component for each entry in this array, as follows. 

We can use array.map, and then we can pass in a function to map that returns a face.

const App = () => array.map(()=>(
  <Face
    width={width}
    height={height}
    centerX={width / 2}
    centerY={height / 2}
    strokeWidth={10}
    eyeOffsetX={30}
    eyeOffsetY={30}
    eyeRadius={10}
    mouthWidth={10}
    mouthRadius={40}
  />
));

And now we’ve got five faces. Let’s say we want a certain number of faces. We can say const array = range(); from D3. Let’s import { range } from D3. And if we say range(5), then we get an array with five entries. If we say range(50), we get an array with 50 entries. How many faces can we fit here? We’ve got 5 by 3, so I’ll just say range(5 * 3), and it’s getting a bit cut off at the bottom. Let me just reduce the width and height a little bit. Whoa now it’s 6 by 3. 

const width = 160;
const height = 160;

const array = range( 6 * 3);

No problem. Now that we’ve got this setup, we can introduce random variation. 

Using randomness to generate graphics

For example, the eyeRadius, we can set to be 10 plus some random number. Math.random gives us a random number between 0 and 1 so if I multiply by 5, now we see some variance, some variation in these eye sizes.  eyeRadius={5 + Math.random() * 5} If we make it {5 + Math.random() * 10}, we get even a wider variety. We can do something similar for mouthWidth, and mouthRadius. Also for eyeOffsetX and eyeOffsetY, strokeWidth

const App = () => array.map(()=>(
  <Face
    width={width}
    height={height}
    centerX={width / 2}
    centerY={height / 2}
    strokeWidth={6 + + Math.random() * 3}
    eyeOffsetX={30 + Math.random() * 9}
    eyeOffsetY={30 + Math.random() * 15}
    eyeRadius={5 + Math.random() * 5}
    mouthWidth={7 + Math.random() * 9}
    mouthRadius={30 + Math.random() * 10}
  />
));

There we have it. That’s how you can use React components and ES6 modules to compartmentalize complexity and express higher-level thoughts, like, let’s make a bunch of random faces using these techniques. We have been able to transform what was once a smelly, dirty mess of code, into a crystalline palace of well-isolated logic. These techniques allow us to compartmentalize complexity, and this allows complexity to scale. And this will become extremely useful when we start to make more complex things, like interactive data visualizations.