This workshop contains exercises to help you understand how to create and test applications using React library.
Every exercise has a corresponding branch exercise-x
(where x
equals exercise number) with setup code.
In case you get lost you can checkout setup code for the next exercise, which is the solution for the previous one.
In order to create a new project using create-react-app
CLI invoke the following command, where my-app
is the name of the directory that will be created and --template typescript
enables TypeScript support:
npx create-react-app my-app --template typescript
For users with npm
version < 5.1
:
npm install -g create-react-app
create-react-app my-app --template typescript
To avoid worrying about CSS append the following link
with Bootstrap 4 CSS to head
section in public/index.html
file.
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css"/>
Development server can be started using
npm start
Other useful commands can be found inside package.json
file:
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
Open src/App.tsx
and edit its contents in order to produce the following output:
<div class="container py-5">
<header class="mb-5">
<h1 class="d-flex justify-content-between align-items-center">
<span>React Tutorial App</span>
<small class="text-muted">v. 03/03/2019</small>
</h1>
<hr>
</header>
<div class="row justify-content-center">
TODO
</div>
</div>
While using smart IDEs like IntelliJ we can notice the following requirements of JSX:
class
is reserved keyword in Javascript and needs to be replaced withclassName
(another example isfor
wherehtmlFor
should be used instead)- JSX is more restrictive than HTML and does not support self-closing tags like
<hr>
- you need to use<hr />
instead
Next, try to replace 03/03/2019 with the current date. You can obtain it using the following snippet. Expressions in JSX are embedded using {}
- eg. {"1 + 1 is " + 2}
new Date().toLocaleDateString()
Also, if you want you can now remove src/logo.svg
and src/App.css
files along with their imports in src/App.tsx
- we won't need them anymore.
Let's start by moving <header className="mb-5">
along with its children to separate file src/Header.tsx
:
import React from 'react';
const Header: React.FunctionComponent = () => (
<header className="mb-5">
<h1 className="d-flex justify-content-between align-items-center">
<span>React Tutorial App</span>
<small className="text-muted">v. { new Date().toLocaleDateString() }</small>
</h1>
<hr />
</header>
);
export default Header;
First, we need to import React from react
package installed via npm
. Then, we define Header
variable of type React.FunctionComponent
which is basically arrow function with no arguments that returns JSX. Finally, we export Header
component from the file so that it can be imported elsewhere.
Arrow functions (aka lambdas) in Javascript:
// Traditional, anonymous function
const fn1 = function(name) {
console.log(`Hello ${name}!`);
}
// Arrow function
const fn2 = (name) => {
console.log(`Hello ${name}!`);
}
// Arrow function with single argument and single expression (both pairs of brackets can be skipped)
const fn3 = name => console.log(`Hello ${name}!`);
Going back to src/App.tsx
we can now replace <header className="mb-5">...</header>
with <Header />
, as long as we import it beforehand using import Header from './Header.tsx'
.
Now, create a new Game
component inside src/Game.tsx
with following output and replace TODO
inside App
component with <Game />
<div>
<div className="card">
<div className="d-flex">
<div className="p-5">
<div className="board">
<button type="button" className="tile btn btn-success">O</button>
<button type="button" className="tile btn btn-info">X</button>
<button type="button" className="tile btn btn-success">O</button>
<button type="button" className="tile btn btn-info">X</button>
<button type="button" className="tile btn btn-success">O</button>
<button type="button" className="tile btn btn-info">X</button>
<button type="button" className="tile btn btn-success">O</button>
<button type="button" className="tile btn btn-outline-secondary" />
<button type="button" className="tile btn btn-outline-secondary" />
</div>
</div>
</div>
</div>
</div>
Contents of index.css
should be replaced with the following CSS in order to apply missing styles:
.tile {
width: 100px;
height: 100px;
font-size: 2rem;
margin: 5px;
}
.board {
width: 330px;
margin: -5px;
}
Let's avoid code duplication in Game
component by creating reusable Tile
component:
import React from 'react';
export enum TileType {
EMPTY,
CIRCLE,
CROSS
}
interface TileProps {
type: TileType;
}
const TEXT = {
[TileType.EMPTY]: '',
[TileType.CIRCLE]: 'O',
[TileType.CROSS]: 'X',
};
const COLOR = {
[TileType.EMPTY]: 'outline-secondary',
[TileType.CIRCLE]: 'success',
[TileType.CROSS]: 'info',
};
const Tile: React.FunctionComponent<TileProps> = (props) => (
<button
type="button"
className={`tile btn btn-${COLOR[props.type]}`}
>
{TEXT[props.type]}
</button>
);
export default Tile;
Notice that we replaced React.FunctionComponent
with parametrized React.FunctionComponent<TileProps>
- that's because our component will receive data through so-called props
which we parametrized using TypeScript.
A common practice is to use object destructuring and replace props
with { type }
so that we can use type
instead of props.type
later in the code. Here are few examples of object destructuring
:
const user = { name: "John", surname: "Doe", address: { city: "New York", zipCode: "10055" } };
const { name, surname, address } = user; // name = "John", surname = "Doe", address = { city: "New York", zipCode: "10055" }
const { address: { city, zipCode } } = user; // city = "New York", zipCode = "10055"
const { name: nameAlias } = user; // nameAlias = "John"
Another useful feature that we used is string interpolation
:
const name = "Pawel";
console.log(`Hello ${name}!`) // Hello Pawel!
Another improvement would be to use array
to dynamically render tiles. Here's how to do it in JSX using Array.prototype.map:
<div>
{['a', 'b', 'c'].map((element, index) => (
<span>I am {element} at index {index}</span>
))}
</div>
Here's prefilled array of tiles:
const board = [
TileType.CIRCLE, TileType.CROSS, TileType.CIRCLE,
TileType.CROSS, TileType.CIRCLE, TileType.CROSS,
TileType.CIRCLE, TileType.EMPTY, TileType.EMPTY
];
Until React 16.8 holding state in components was acomplished by using Javascript classes which extended React.Component
. Below is a minimal example of using state
and handlers - counter with button for incrementing value:
interface State {
counter: number;
}
class StatefulComponentExample extends React.Component<any, State> {
state = {
counter: 0
};
handleClick = () => this.setState(prevState => ({
counter: prevState.counter + 1
}));
render() {
return (
<div>
Current value: {this.state.counter} <br />
<button onClick={this.handleClick}>Increment</button>
</div>
);
}
}
First of all, if we wanted to use state, our component needed to be a class. In class components we can access props
and state
using this.props
and this.state
. Adding handler to button is very similar to old HTML syntax, except that we are passing function reference as an argument and the name of the attribute is onClick
, not onclick
. State mutations in class components are handled via this.setState method.
In our application, we need to create stateful component for storing board state. Let's start with the following code in src/GameContainer.tsx
:
interface GameState {
board: TileType[];
}
class GameContainer extends React.Component<any, GameState> {
render() {
return (
<Game board={this.state.board} />
);
}
}
In case you are wondering why GameContainer
- Dan Abramov, one of the React core contributors, once introduced the idea of splitting code between so-called Dumb and Presentational components.
Then, we need to initialize our state with value, as in previous example. Let's move there board
from src/Game.tsx
. Additionally, we need to change Game
so that it uses board
passed via props
.
Next, let's add some onClick
handler which will set clicked Tile
state to X:
handleSelect = (index: number) => {
this.setState(state => {
const board = [...state.board];
board[index] = TileType.CROSS;
return {
board
};
});
};
Then, we need to pass it down to Game
and even further to Tile
- but how to pass index
value? Using anonymous inline functions:
<Tile onClick={() => onSelect(index)} />
Finally, we need to decide whether it's X or O's turn. Simple solution would be to extend our state with boolean flag: circleIsNext
, which we would flip inside select
. However, that would violate so-called single source of truth
principle which means that we could end up havign conflicted data.
A more robust solution would be to count the number of empty tiles in our board - if it's even it could mean that it's O's turn.
As I mentioned earlier, until React 16.8 class components were the only way to handle state natively in React components. This version introduced a groundbreaking change - React hooks.
With hooks, you can achieve almost everything that class components allowed, using simpler syntax and better code locality.
Here is an example of the same counter as before:
const StatefulComponentExampleWithHooks: React.FunctionComponent = () => {
const [counter, setCounter] = React.useState(0);
return (
<div>
Current value: {counter} <br />
<button onClick={() => setCounter(counter + 1)}>Increment</button>
</div>
);
}
As an additional exercise you can try and implement GameContainer
using hooks.
It is not possible to use traditional if
statement inside JSX tree because it does not return value. However, we can use this operator cheatsheet instead:
// conditional rendering
<div>
{flag && (
<Component />
)}
</div>
// if-elseif rendering
<div>
{flag && (
<ComponentIfTrue />
) || anotherFlag && (
<ComponentIfFalse />
)}
</div>
// this also works
<div>
{flag ? (
<ComponentIfTrue />
) : (
<ComponentIfFalse />
)}
</div>
In our application we want to display information about the winner. But first we need to calculate if we have one. We will do it using the following function which we will add in src/utils.ts
:
export function calculateWinner(board: TileType[]): TileType | null {
const lines = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6],
];
for (let i = 0; i < lines.length; i++) {
const [a, b, c] = lines[i];
if (board[a] && board[a] === board[b] && board[a] === board[c]) {
return board[a];
}
}
return null;
}
With this information, we can pass it to the Game
component and display the following alert, which of course should be wrapped with condition and parametrized:
<div class="alert alert-success text-center">
The winner is: <strong>Player O</strong>!
</div>
Additionaly, we can show draw message if there is no winner and there are 0 empty tiles:
<div class="alert alert-warning text-center">
It's a draw!
</div>
We want to add some routing to our application. First, we need to install router and its typings:
npm install react-router-dom --save
npm install @types/react-router-dom --save-dev
Now we can replace our <GameContainer />
inside src/App.tsx
with router's Switch
component:
<Switch>
<Route path="/game" component={GameContainer}/>
<Route path="/weather/:city?" component={WeatherContainer}/>
<Route component={Home}/>
</Switch>
For better code organisation, let's move all game-related files into Game
directory and create Weather
and Home
for the rest of the routes. Then we can create some empty components in those directories in order to compile our code.
This is the code for src/Home/index.tsx
, which will be our home route. It will display cards with links to other routes:
import React from 'react';
import Card from "./Card";
const Home: React.FunctionComponent = () => (
<React.Fragment>
<Card
title="Tic-tac-toe"
button="Play a game"
url="/game"
/>
<Card
title="Open Weather API"
button="Get forecast"
url="/weather"
/>
</React.Fragment>
);
export default Home;
You may notice new type of component here - React.Fragment
. Because JSX is a tree-like structure, you may only have one root node. In our example we want to have 2 cards so in order to do it we need to wrap them with some other component. React.Fragment
does not render any HTML so it's basically a wrapper for returning multiple nodes.
Next we need to implement src/Home/Card.tsx
to return the following parametrized code:
<div class="col-3">
<div class="card text-center">
<div class="card-body">
<h5 class="card-title">Tic-tac-toe</h5>
<a class="btn btn-primary" href="/game">Play a game</a>
</div>
</div>
</div>
Links in react-router
can be created using Link
component.
Let's start implementing our Weather
component:
Form (src/Weather/WeatherForm.tsx
):
import React from 'react';
interface WeatherFormProps {
}
const WeatherForm: React.FunctionComponent<WeatherFormProps> = () => (
<form>
<div className="form-row align-items-center">
<div className="col">
<input type="text" className="form-control" placeholder="City" />
</div>
<div className="col-auto">
<button type="submit" className="btn btn-primary">Get forecast</button>
</div>
</div>
</form>
);
export default WeatherForm;
Weather (src/Weather/Weather.tsx
):
import React from 'react';
import WeatherForm from "./WeatherForm";
interface WeatherProps {
}
const Weather: React.FunctionComponent<WeatherProps> = () => (
<div className="col-6">
<div className="card">
<div className="card-header">
Weather forecast
</div>
<div className="card-body">
<WeatherForm />
</div>
</div>
</div>
);
export default Weather;
Finally, we can move to WeatherContainer
. It needs to be class component, as it will keep form values in its state.
Interface for our data:
export interface Forecast {
main: {
temp: number;
pressure: number;
humidity: number;
}
}
Util function for making an AJAX request:
export function loadForecast(city: string): Promise<Forecast> {
return new Promise<Forecast>((resolve, reject) => {
const params = new URLSearchParams();
params.set("q", city);
params.set("units", "metric");
params.set("appid", process.env.REACT_APP_API_KEY || "");
fetch(`https://api.openweathermap.org/data/2.5/weather?${params.toString()}`)
.then(response => {
if (response.ok) {
response.json().then(resolve);
} else {
reject();
}
})
.catch(reject);
});
}
Next, we need to create .env
file with REACT_APP_API_KEY=<secret value>
and restart our application.
In React we usually make AJAX request inside componentDidMount
lifecycle method. Using our util, we can write the following code:
loadForecast(this.state.city)
.then(forecast => {
// on success
})
.catch(() => {
// on error
})
.finally(() => {
// in both cases
});
To make our component user-friendly, we should add loading and error indicators. Once again it can be achieved easily with state variables loading
and error
. Then we can push them down to Weather
component and use as this:
{error && (
<div className="alert alert-danger my-3" role="alert">Loading forecast data failed!</div>
)}
{loading && (
<div className="text-center p-5" data-testid="spinner">
<div className="spinner-border text-primary" role="status">
<span className="sr-only">Loading...</span>
</div>
</div>
) || forecast !== null && (
<div className="row text-center">
<dl className="mt-3 mb-0 col-4">
<dt>Temperature</dt>
<dd>{forecast.main.temp} °C</dd>
</dl>
<dl className="mt-3 mb-0 col-4">
<dt>Pressure</dt>
<dd>{forecast.main.pressure} hPa</dd>
</dl>
<dl className="mt-3 mb-0 col-4">
<dt>Humidity</dt>
<dd>{forecast.main.humidity}%</dd>
</dl>
</div>
)}
Once again, you can try and implement this container using hooks.
In this exercise we will be testing our Weather component.
create-react-app
out of the box is configured with test utils: jest
and react-testing-library
.
For mocking network requests we will use this primitive code:
export const mockNoResponse = () => jest
.spyOn(global as any, 'fetch')
.mockImplementation(() => new Promise(() => {}));
export const mockOkResponse = () => jest
.spyOn(global as any, 'fetch')
.mockImplementation(() => Promise.resolve({
ok: true,
json: () => Promise.resolve({
"main": {
"temp": 293.25,
"pressure": 1019,
"humidity": 83,
"temp_min": 289.82,
"temp_max": 295.37
}
}
)
}));
export const mockErrorResponse = () => jest
.spyOn(global as any, 'fetch')
.mockImplementation(() => Promise.resolve({
ok: false
}));
Tests to write:
describe('Weather Component', () => {
describe('without city in path', () => {
it('should render empty input', () => {
// TODO
});
it('should not load the forecast', () => {
// TODO
});
});
describe('with city in path', () => {
it('should render city name in input', () => {
// TODO
});
it('should load the forecast', () => {
// TODO
});
});
describe('form submit', () => {
it('should call the API with provided city', () => {
// TODO
});
it('should change page url to contain city name', () => {
// TODO
});
});
describe('forecast loading', () => {
it('should disable the form', () => {
// TODO
});
it('should display spinner', () => {
// TODO
});
});
describe('forecast loaded', () => {
it('should enable the form', () => {
// TODO
});
it('should hide the spinner', () => {
// TODO
});
it('should display error on failure', () => {
// TODO
});
it('should display data on success', () => {
// TODO
});
});
});