If you have been somewhat active in a development community, you probably have already heard about React. I stepped aside of it for quite a long time, but now I don’t have any alternative: I need to mingle with React code at work.
In this tutorial, I will be showing you how to use its recommended basic building blocks: the function components. I will do so by building a simple Checkers board.
Here’s a quick summary for what you’ll learn :
- The JSX syntax, which resembles to HTML with subtle differences.
- How to reuse and parameterize our components through component properties, so we can avoid duplication in our code.
- How to pass data from higher level components to lower level components, by using a top-down data flow.
- How to create nested components, so we can compose our views with a greater flexibility.
Are you ready to dive in?
Project Setup
Let’s start with a real quick setup.
I advise you to use the create-react-app
1 package that greatly simplifies the setup of any React application. The only prerequisite is to have a recent version of Node.js2 installed on your development environment, which I won’t cover in this tutorial (but the official documentation explains it very well).
Then you will have to use the following commands in your terminal (from the folder where you want to store your app files):
Both npx
and npm
commands come with Node.js default packages. The start
script is created by create-react-app
and will run your project at the address localhost:3000
.
If you keep your browser open during development, you will see that the changes you made in the project files will automatically trigger a refresh of the page. This is the hot reload mode from webpack-dev-server
that has been automatically installed with the command create-react-app
.
You don’t have to configure anything more. So we’re ready to jump into development.
Components are not HTML
If you open the src/App.js
file, you will notice the following code:
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
export default App
This markup language may seem familiar, but you have probably noticed something unusual: that className
attributes is not part of the valid HTML syntax. Indeed, React components are written in React’s own markup language: JSX.
You can think of it as a javascript templating engine for HTML. All native javascript functions and API will be available, but the HTML part has to be compiled by React in order to be rendered by the browser. This is part of the tooling that create-react-app
has automatically installed and configured for us.
If you step up in the src/index.js
you will see the following code:
import `App` from './App'
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
This file is the entry point of our application, this is where React components are inserted into the DOM.
The <App />
component will be replaced by the JSX markup we have seen defined in the src/App.js
file.
The whole JSX markup passed to the ReactDOM.render()
method will be appended to the element with the id root
in our page.
Let’s modify this component to create our Checkers board:
function App() {
return (
<table className='no-border'>
<thead>
<tr><th></th><th>a</th><th>b</th><th>c</th><th>d</th><th>e</th><th>f</th><th>g</th><th>h</th><th></th></tr>
</thead>
<tbody>
<tr><th>8</th><td className='square light'></td><td className='square dark'></td><td className='square light'></td><td className='square dark'></td><td className='square light'></td><td className='square dark'></td><td className='square light'></td><td className='square dark'></td><th>8</th></tr>
<tr><th>7</th><td className='square dark'></td><td className='square light'> /* ... */</tr>
/* ... */
</tbody>
<tfoot>
<tr><th></th><th>a</th><th>b</th><th>c</th><th>d</th><th>e</th><th>f</th><th>g</th><th>h</th><th></th></tr>
</tfoot>
</table>
);
}
And add the needed styling in src/App.css
:
.no-border {
border-collapse: collapse;
}
.square {
width: 83px;
height: 83px;
}
.light {
background-color: #FFCE9E;
}
.dark {
background-color: #D18B47;
}
You might appreciate or not the use of a <table>
element to represent our board, but you will certainly agree that the actual code contains a lot of repetition, which makes it harder to read and to modify. Let’s change this.
Components are reusable
From the previous code, we can identify two types of repeated elements: the rows (tr
) and the cells (td
): this will make good candidates to be transformed into components.
Let’s create a component for our rows first:
function Row(props) {
const isEven = props.number % 2 === 0
return (
<tr>
<th>{props.number}</th>
{isEven ? <td className='square light'></td> : null}
<td className='square dark'></td>
<td className='square light'></td>
<td className='square dark'></td>
<td className='square light'></td>
<td className='square dark'></td>
<td className='square light'></td>
<td className='square dark'></td>
{isEven ? null : <td className='square light'></td>}
<th>{props.number}</th>
</tr>
)
}
export default Row
We need to declare the function component that returns the JSX markup corresponding to one row on our Checkers board.
There are two ways one row differs from another:
- Each row has an associated number.
- Even rows start by a light square, and odd rows start by a dark square.
Have you noticed the props
argument that is passed to our function component? This behavior of React let us pass values from high level components to lower level components.
This is how it looks like from the App
component:
import Row from './components/row'
function App() {
return (
<table className='no-border'>
<thead>
<tr><th></th><th>a</th><th>b</th><th>c</th><th>d</th><th>e</th><th>f</th><th>g</th><th>h</th><th></th></tr>
</thead>
<tbody>
{[8, 7, 6, 5, 4, 3, 2, 1].map(number => <Row key={number.toString()} number={number} />)}
</tbody>
<tfoot>
<tr><th></th><th>a</th><th>b</th><th>c</th><th>d</th><th>e</th><th>f</th><th>g</th><th>h</th><th></th></tr>
</tfoot>
</table>
);
}
But you can notice that I added two properties to this component:
key
is a default attribute required by React when several components are inserted through an iteration, it needs to be different for each element in the application.number
is a custom attribute. React will extract the value of each attribute that is not a default one, and map these values as members of theprops
parameter that is passed to the component function.
This behavior of React components let us write the markup for each component only once, and then compute them with different properties each time we need to add a new one.
Note: Because JSX markup can only compute expressions inside its
{}
, we couldn’t use afor
loop instead of theArray.prototype.map()
call.
Exercise 1: Before we continue this tutorial, could you extract the Square
component (a singular cell on the Checkers board), just as we did with the row
component?
You can find the source code on GitHub. As well as my solution
Data flows top-down through components
Now that we create those Row
and Square
components dynamically, how would we place our Checkers pieces inside specific squares on the board?
The only solution would be to store that data in the top level component: the App
itself. We will use a two-dimensional array, where we’ll store the pieces as o
letters.
The white player pieces will be in upper case, and the black player pieces in lower case. The empty squares will be represented by spaces.
function App() {
const data = [
[' ', 'o', ' ', 'o', ' ', 'o', ' ', 'o'],
['o', ' ', 'o', ' ', 'o', ' ', 'o', ' '],
[' ', 'o', ' ', 'o', ' ', 'o', ' ', 'o'],
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
['O', ' ', 'O', ' ', 'O', ' ', 'O', ' '],
[' ', 'O', ' ', 'O', ' ', 'O', ' ', 'O'],
['O', ' ', 'O', ' ', 'O', ' ', 'O', ' ']
]
return (
<table className='no-border'>
<thead>
<tr><th></th><th>a</th><th>b</th><th>c</th><th>d</th><th>e</th><th>f</th><th>g</th><th>h</th><th></th></tr>
</thead>
<tbody>
{data.map((rowData, index) => {
const number = data.length - index
return <Row key={number.toString()} number={number} data={rowData} />
})}
</tbody>
<tfoot>
<tr><th></th><th>a</th><th>b</th><th>c</th><th>d</th><th>e</th><th>f</th><th>g</th><th>h</th><th></th></tr>
</tfoot>
</table>
);
}
You can see how we have changed the data we iterate over. Now that each row contains actual data, we need to compute the rows numbers from the index
parameter that is passed to javascript’s native Array.prototype.map()
function.
Then each row has to split the data received, so we can iterate a second time to generate the squares:
function Row(props) {
return (
<tr>
<th>{props.number}</th>
{props.data.map((squareData, index) => {
const column = String.fromCharCode(97 + index)
return <Square key={column + props.number} row={props.number column={column} data={squareData} />
})}
<th>{props.number}</th>
</tr>
)
}
The code is a little similar here, except that the index is a letter, computed from an UTF-16 code unit. 3
Now we need to use this data in order to display the Checkers pieces images. Since Checkers basic pieces (called ‘men’) are simple round tokens, we will use simple svg4 files to represent them. Here’s the code for a black man:
<svg
width="64px"
height="64px"
viewBox="-32 -32 64 64"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Checkers black man</title>
<circle id="man" cx="0" cy="0" r="32" stroke="none" fill="#000" />
</svg>
For white men, we just need to change the fill
attribute of the <circle>
element to #fff
(and the <title>
element would be nice to change too).
So our Piece
component could be as simple as the following:
import blackMan from '../assets/black-man.svg'
import whiteMan from '../assets/white-man.svg'
function Piece(props) {
const player = props.data === 'O' ? 'white' : 'black'
return (
<img src={player === 'white' ? whiteMan : blackMan} alt={`A ${player} man.`} className={'piece'} />
)
}
As you can see, we start by importing the svg images inside our component definition. This trick will compute the correct path for our images when our code will be processed by the scripts that create-react-app
has installed for us.
Then we can insert our Piece
component inside the Square
components:
function Square(props) {
// ...
return (
<td className={'square ' + (isLight ? 'light' : 'dark')}>
{props.data.trim() && <Piece data={props.data} /> }
</td>
)
}
Note that we don’t want to include a piece in every square, that’s why we are checking that props.data
is empty when trimmed for spaces. We do that by taking advantage of javascript lazy evaluation in conditional structure: if the left side of my &&
(logical AND) evaluates to false, then the right side is not evaluated.
Components are nestable
So far, that could be it for our Checkers board. We could easily add the ‘King’ pieces by creating two more svg images, adding one more condition in our Piece
component and modifying the data in our App
component…
Did you noticed that the components we would have to modify are at the two extremities of our components stack?
In their best selling book, Refactoring: Improving the Design of Existing Code 5, Martin Fowler and Kent Beck, have labeled this particular anti-pattern shotgun surgery.
Because the related code is spread over the components, if we want to make a change, we will have to search into multiple files for the parts we have to modify. But the code doesn’t have to be written that way.
We could start by moving our Piece
component up to the Row
:
import Piece from './Piece'
function Row(props) {
// ...
return (
<Square
key={column + props.number}
row={props.number}
column={column}
>
{squareData.trim() && <Piece data={squareData} />}
</Square>
)
// ...
}
Can you see how the Piece
component is nested between the two <Square>
tags?
To retrieve it from inside the Square
component, we have to access a React components’ special property: children
.
function Square(props) {
// ...
return (
<td className={'square ' + (isLight ? 'light' : 'dark')}>
{props.children}
</td>
)
}
Now our Square
component can display any JSX markup we nest inside it.
But we won’t stop here. In fact we could modify our Row
component even further, to not take an array of characters, but directly an array of React components.
function Row(props) {
return (
<tr>
<th>{props.number}</th>
{props.data.map((Piece, index) => {
const column = String.fromCharCode(97 + index)
return (
<Square
key={column + props.number}
row={props.number}
column={column}
>
{Piece && <Piece/>}
</Square>
)
})}
<th>{props.number}</th>
</tr>
)
}
Here, we don’t import the Piece
component from the Piece.js
file anymore. Instead we get it by iterating over the data
property that is passed to our Row
component.
Then, if it is not a falsy value (like null
), we compute whatever component it might be. But we cannot pass it a property value from here. Instead, we need to separate our pieces into different components.
import blackMan from '../../assets/black-man.svg'
function BlackMan(props) {
return (
<img src={blackMan} alt={'A black man.'} className={'piece'} />
)
}
import whiteMan from '../../assets/white-man.svg'
function WhiteMan(props) {
return (
<img src={whiteMan} alt={'A white man.'} className={'piece'} />
)
}
And then we change our two-dimensional array from the App
component to populate it with function components.
function App() {
const data = [
[null, BlackMan, null, BlackMan, null, BlackMan, null, BlackMan],
[BlackMan, null, BlackMan, null, BlackMan, null, BlackMan, null],
[null, BlackMan, null, BlackMan, null, BlackMan, null, BlackMan],
[null, null, null, null, null, null, null, null],
[null, null, null, null, null, null, null, null],
[WhiteMan, null, WhiteMan, null, WhiteMan, null, WhiteMan, null],
[null, WhiteMan, null, WhiteMan, null, WhiteMan, null, WhiteMan],
[WhiteMan, null, WhiteMan, null, WhiteMan, null, WhiteMan, null]
]
return (
<table className='no-border'>
// ...
<tbody>
{data.map((rowData, index) => {
const number = data.length - index
return <Row key={number.toString()} number={number} data={rowData} />
})}
</tbody>
// ...
</table>
);
}
Exercise 2: How would you implement the King pieces now?
How I learned
I didn’t learn all of this by myself though. I used a shortcut: a React expert’s book.
The book I have used during my very first week of learning React is React Projects6, by Roy Derks. This book has a very practical approach that takes you by the hand through its project based learning. During my 8 first hours with it, I built:
- A simple movie list.
- A portfolio that fetches data from the GitHub API.
- A project management board with a drag’n’drop interface.
Building projects is a great way of learning new technologies and practices. It keeps you motivated by envisioning a goal.
If the board game theme motivates you more, here are some ideas to tweak the above projects:
- Your favorite board games list
- Fetching the most popular board games from Board Game Geek’s API7
- A tournament scheduler where you drag’n’drop the players as matches are played
Enjoy your learning, and nest all those components!
- Create React App, accessed December 4, 2020, https://create-reac-app.dev↩︎
- NodeJs.org, accessed December 4, 2020, https://nodejs.org↩︎
- String.fromCharCode(), JavaScript Reference, Mozilla Developers Network, last modified December 18, 2020, ↩︎
- SVG: Scalable Vector Graphics, Mozilla Developers Network, last modified December 30, 2020, https://developer.mozilla.org/en-US/docs/Web/SVG↩︎
- Martin Fowler, Kent Beck, Refactoring: Improving the Design of Existing Code, 2nd edition (Boston: Addison-Wesley Professional, 2018).↩︎
- Roy Derks. React Projects. Birmingham, UK: Packt Publishing, 2019.↩︎
- “BGG XML API2”, Wiki, Board Game Geek,last edited December 28, 2016, https://boardgamegeek.com/wiki/page/BGG_XML_API2↩︎
Leave a Reply
You must be logged in to post a comment.