Datavis 2020 Episode 9 – Let’s Make a Face Part V multiple files with ES6 modules

We’re going to make our face composed in index.js from multiple files – backgroundCircle.js, Eyes.js, and Mouth.js, which will import from d3.arc

We’ll discuss 

Refactoring React components
Using many files with ES6 Modules
Named exports vs. default exports
Using React Fragments
Composing the face from components –  BackgroundCircle, Eyes, and Mouth

Refactoring React components

I would like to put this BackgroundCircle component into a separate module, into a separate file, but the one thing that’s holding me back is that it refers to strokeWidth, which is defined in this module somewhere. So let me also make this a prop. We can do that by just adding another element here in our list of variables that are getting destructured from the props at marker 1 in our code snippet below. And then down here, in the App component, we can pass in strokeWidth= {strokeWidth}

//                                  |-----1----|
const BackgroundCircle = ({ radius, strokeWidth }) => (
  <circle
    r={radius}
    fill="yellow"
    stroke="black"
    stroke-width={strokeWidth}
  />
);

const App = () => (
  <svg width={width} height={height}>
    <g transform={`translate(${centerX},${centerY})`}>
 //                                                        |----2-----| |-----3----|
      <BackgroundCircle radius={centerY - strokeWidth / 2} strokeWidth={strokeWidth} />
      <circle cx={-eyeOffsetX} cy={-eyeOffsetY} r={eyeRadius} />
      <circle cx={eyeOffsetX} cy={-eyeOffsetY} r={eyeRadius} />
      <path d={mouthArc()} />
    </g>
  </svg>
);

So this strokeWidth in the code snippet above at marker 2 is defining the prop, and this strokeWidth at marker 3 is accessing the variable that has that value.

Using many files with ES6 Modules

Alright, now we’re in a position to move this component into a separate module. Next, I’m going to create a new file. I’ll call it BackgroundCircle.js. I like to have the file name the same as the component name, and now I can just cut this code out of here, and paste it into this file. Now, we can use ES6 modules, so here, we’re defining a module, and I want to export this. So to do that, we can say 

export const BackgroundCircle = ({ radius, strokeWidth }) => (
  <circle
    r={radius}
    fill="yellow"
    stroke="black"
    stroke-width={strokeWidth}
  />
);

And that gives us a named export from this module. 

Named exports vs. default exports

And then over an index.js, we can import it like this. import { BackgroundCircle } from './BackgroundCircle';

          |———————1—————|
import { BackgroundCircle } from './BackgroundCircle';

We’re using curly braces at marker 1 in the code snippet above because it’s a named export. Sometimes you might see this syntax – with no curly braces. import BackgroundCircle from './BackgroundCircle'; This works if you use a default export. You can do that like this: 

export default ({ radius, strokeWidth }) => (
  <circle
    r={radius}
    fill="yellow"
    stroke="black"
    stroke-width={strokeWidth}
  />
);

But I personally prefer named exports, and I think it’s sort of best practice to use mainly named exports, because I find this confusing. Because you can’t correlate the import name in index.js to anything in here. And you could rearrange the code like this to say..

const BackgroundCircle = ({ radius, strokeWidth }) => (
  <circle
    r={radius}
    fill="yellow"
    stroke="black"
    stroke-width={strokeWidth}
  />
);
export default BackgroundCircle;

But then with the default export, these names in index.js could become out of sync. You could use any name here, and it wouldn’t matter. So that’s why I like to use named exports. 

export const BackgroundCircle = ({ radius, strokeWidth }) => (
  <circle
    r={radius}
    fill="yellow"
    stroke="black"
    stroke-width={strokeWidth}
  />
);

And then when we import it in index.js, we put it in the curly braces. import { BackgroundCircle } from ‘./BackgroundCircle';

Alright! We’ve successfully taken full advantage of React components and ES6 modules for this one thing – BackgroundCircle. Let’s do the same kind of refactoring for the other parts of our App component in index.js – starting with these two circles here that are the eyes. 

Using React Fragments

So I think I’ll make a new component called Eyes. const Eyes equals a function that returns some JSX, and I’m just going to paste that JSX which makes those circles into here. 

const Eyes = () => (
<circle cx={-eyeOffsetX} cy={-eyeOffsetY} r={eyeRadius} />
<circle cx={eyeOffsetX} cy={-eyeOffsetY} r={eyeRadius} />
);

Now we’re getting an interesting SyntaxError. 

Failed to compile
SyntaxError: Adjacent JSX elements must be wrapped in an enclosing tag (18:0)
/index.js (line 18)
14 : const mouthRadius = 140;
15 : 
16 : const Eyes = () => (
17 : 
18 : 
     ^

It says “adjacent JSX elements must be wrapped in an enclosing tag”. This is one of the gotchas with JSX. And what it means is that you can’t return two circle elements like this from one component. They need to be wrapped in something. Typically, you could wrap it in a <div>, but that results in extra DOM elements, and now we’re inside SVG, so it doesn’t really fit. It wouldn’t work. The equivalent to a <div> in SVG is a group element. So we could do it like this. 

const Eyes = () => (
<g>
<circle cx={-eyeOffsetX} cy={-eyeOffsetY} r={eyeRadius} />
<circle cx={eyeOffsetX} cy={-eyeOffsetY} r={eyeRadius} />
</g>
);

Where we have a beginning group <g>, and an end group </g>. And these circles go inside of this group element. This does work, but there is a cleaner way to do this that doesn’t require creating any new DOM elements. And that is to use React fragments. And you can use a React fragment like this, by creating these weird-looking sort of empty JSX elements. 

const Eyes = () => (
<>
<circle cx={-eyeOffsetX} cy={-eyeOffsetY} r={eyeRadius} />
<circle cx={eyeOffsetX} cy={-eyeOffsetY} r={eyeRadius} />
</>
);

These create React fragments. This solution satisfies the need to return a single thing from a React component, but it’s also cleaner than introducing group elements because those group elements don’t really need to be there. So that’s how you can solve that error if you ever see it in the future. 

We can’t move this to a separate module yet, because we’re referring to these variables in these <circle> elements that exist outside of the component. We can fix that by introducing these as props. 

const Eyes = ({eyeOffsetX, eyeOffsetY, eyeRadius }) => (
<>
<circle cx={-eyeOffsetX} cy={-eyeOffsetY} r={eyeRadius} />
<circle cx={eyeOffsetX} cy={-eyeOffsetY} r={eyeRadius} />
</>
);

So we’ve got eyeOffsetX, eyeOffsetY, and eyeRadius. Our component accepts these props, but our outer component is not passing these in. So let’s make that happen. 

In the App component, let’s add

const App = () => (
  <svg width={width} height={height}>
    <g transform={`translate(${centerX},${centerY})`}>
      <BackgroundCircle radius={centerY - strokeWidth / 2} strokeWidth={strokeWidth} />
      <Eyes
        eyeOffsetX={eyeOffsetX}
        eyeOffsetY={eyeOffsetY}
        eyeRadius={eyeRadius}
        />
      <path d={mouthArc()} />
    </g>
  </svg>
);

All right, now we can move this into a separate module. I’ll make a new file called Eyes.js, and I’ll cut this code out of here paste it into Eyes.js, add the export keyword, and then over in index.js, import {Eyes} from "./Eyes"; 

You could put .js here, but you don’t need to. 

Composing the face from components –  BackgroundCircle, Eyes, and Mouth

All right, I think the last remaining thing is the mouth. I’m going to make this <path d={mouthArc()} /> a component as well. I’ll call it <Mouth/> in the App component, and then introduce Mouth as a component that returns that JSX. 

const Mouth = () => (
  <path d={mouthArc()} />;
  )

And we’re referring to mouthArc. What we could do is pass that in as a prop. 

const Mouth = ({mouthArc}) => (
  <path d={mouthArc()} />;
  )

But I don’t really want to go that route, because I think it’s asking too much of the parent component. Too much understanding of the internals. All the parent component really needs to know is the mouthRadius, and mouthWidth. So instead of making the App component come up with this D3 arc generator, which I think is too detailed, instead, I’m going to make mouthRadius and mouthWidth props now. 

const Mouth = ({mouthRadius, mouthWidth }) => (
  <path d={mouthArc()} />;
  )

We can make this component responsible for coming up with this arc. So to do that, I’m going to move away from these parentheses, and instead use curly braces. 

const Mouth = ({mouthRadius, mouthWidth }) => {
  <path d={mouthArc()} />;
}

That way we can include many expressions in the body of this function, including the expression that generates this arc generator. 

const Mouth = ({mouthRadius, mouthWidth }) => {
  const mouthArc = arc()
  .innerRadius(mouthRadius)
  .outerRadius(mouthRadius + mouthWidth)
  .startAngle(Math.PI / 2)
  .endAngle((Math.PI * 3) / 2);

  return <path d={mouthArc()} />;
}

And previously, this <path> element was being implicitly returned, but now we need to explicitly say return. I could put parentheses around this, but it’s not strictly required. So I’m not going to do it, because this fits comfortably on a single line now. All we need to do is pass in these props. So let’s make that happen down in our App component. 

<Mouth mouthRadius={mouthRadius} mouthWidth={mouthWidth} />

All right, now we can move Mouth into a separate module, separate file. I’ll cut this code out of there, make a new file called Mouth.js, and then in this file, I’ll paste that code, add the export keyword and then import it in index.js. import { Mouth } from './Mouth';

This does not seem to be working. And we’re not getting any syntax errors. In Vizhub, syntax errors show up here, in the visualization pane. But runtime errors do not. For runtime errors, we need to open the console. 

ReferenceError: arc is not defined

And that makes perfect sense, because we’re importing arc from D3 inside of index.js, but we don’t actually need that in index.js. So I’m going to remove that import from index.js, and then add it to Mouth.js. Now everything works again.

All right! So far so good. We have refactored these components so that we have BackgroundCircle, Eyes, and Mouth. And I find this to be much more readable than what we had before. We have semantic meaning in these names themselves.

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