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.