📖 Ok, so this "exercise" is a big wall of text. We don't like it anymore than you do, but at some point you got to get down to it and learn React, right? So take a deep breath and go through everything in order, step by step. Don't worry, theres plenty of coding coming up!
- Basic React components
- Basic JSX syntax
- Prop Types
📖 There are two ways to define a React component:
a) As a class that extends the Component
base class from React (going to call these class components from now on).
b) As a pure function that take props as input parameter and returns a view (going to call these pure components from now on).
There are some major differences between the two approaches:
- A class component can have internal state, a pure component cannot.
- A class component can have lifecycle hooks/methods, a pure component cannot.
- A class component must have a
render
method. This method must return a view. A pure component is itself the render function and must also return a view. - A pure component is only a simple function that takes data in and returns a view.
- A pure component is faster and simpler to reason about. It's faster because the React algorithm that tries to make smart decisions about what components has changed since the last render cycle and must be swapped, can make a lot of assumptions about how the component might and might not have changed. The runtime can make a lot fewer checks and safeguards to reach it's conclusion.
- We prefer pure components over class components wherever possible, as they are simpler to understand, faster at runtime, and less code to write. Class components are not bad or undesirable, just not as lightweight and elegant.
📖 In the simplest terms, a React component is a function which takes data as its input parameter and returns a view. The most common way to write a view in React is using JSX
syntax, which looks like HTML but really isn't. In most cases you can write HTML as you know it and React will scream in your face when you mess up and have to adjust to JSX syntax.
💡 JSX is just a standard - a specification. It's not part of the React framework. We could write markup in JSX in other SPA (Single Page Application) frameworks as well, but it's most commonly used with React.
className
instead ofclass
to specify CSS classes.- All items in a list must have a
key
prop which must be unique. - Minor differences such as the HTML
<label for="id" />
being<label htmlFor="id" />
in JSX. - Callback functions for events such as clicking a button are always in the form of
onFooEvent
, i.e.:onClick
,onMouseLeave
, etc.
💡 JSX is just syntax sugar on top of React's DOM API. In plain JavaScript you can write
document.createElement(name, children)
to create HTML elements in code. React elements are really written asReact.createElement(name, children)
, but because this is tedious and complicated to write, JSX abstracts this away and we can write<div />
instead ofReact.createElement('div', null)
.
💡 You might need a plugin/extension for your text editor/IDE for it to understand and format JSX correctly.
📖 A simple React component, written as a pure component may look like this:
const Checkout = props => (
<div>
<p>Total sum: {props.totalAmount}</p>
</div>
);
📖 If the above function syntax looks funny to you, here's a more familiar version of the same:
function Checkout(props) {
return (
<div>
<p>Total sum: {props.totalAmount}</p>
</div>
);
}
📖 We do prefer the first version though, as it's less ceremony to write, and const
promotes immutability which makes our code easier to reason about and trust. A good tip is to use const
wherever you used to use var
in JavaScript - for all assignments (functions and variables).
The same component written as a class component:
class Checkout extends Component {
render() {
return (
<div>
<p>Total sum: {this.props.totalAmount}</p>
</div>
);
}
}
- We receive
props
as a parameter in both cases. When written as a pure component, we receive props as the first (and only) function argument. When written as a class component, we get props as a object onthis.props
.
📖 Props are the values/data sent in to our component from the parent component. For example here we pass totalAmount
and address
as props to our inner Summary component:
class Checkout extends Component {
render() {
const amount = calculateTotalAmount(this.props.orderItems);
return (
<Summary totalAmount={amount} address={this.props.customer.address} />
);
}
}
The Summary
component will now be able to read these values from this.props.totalAmount
and this.props.address
.
💡 The
render()
method is a mandatory convention. All React class components must have arender()
method. If your component does not render a view (which is sometimes the case), render can returnnull
, but it must be implemented and return something. When writing pure components, the whole component is itself the render function and it must return a JSX view (or null).
Still with us? Sure hope so!
Found a precise enactment of what it feels like getting through this exercise:
📖 Class components can have internal state in addition to having props sent in. Knowing when to use internal state vs having state passed into the component from the outside via props (for example via state containers such as Redux, which we're going to use) is one of the challenges when you're new to React.
Here's an example where we have the state of a counter as internal state:
class Checkout extends Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
this.incrementCounter = this.incrementCounter.bind(this);
}
incrementCounter() {
this.setState({
count: this.state.count + 1
});
}
render() {
return (
<div>
<span>The counter is: {this.state.count}</span>
<br />
<button value="Increment" onClick={this.incrementCounter} />
</div>
);
}
}
- Note there is some boilerplate ceremony necessary in the constructor now.
- The initial
count
state is set in the constructor. - We are only ever allowed to change the state by using React's
this.setState(newState)
method. If we did anotherthis.state = { count: this.state + 1 }
in theincrementCounter
method we'd get an error. - We bind the incrementCounter method to the instance of the component (this), so that this will point to the correct object (see Passing functions to components)
- In the
render()
method we access thecount
value by usingthis.state.count
. - We hook the button up to our
incrementCounter
method when it's clicked.
📖 Prop Types are React's "poor man's type checking".
Let's say you have the following component:
import React from "react";
const Checkout = props => (
<div>
<p>Total sum: {props.totalAmount}</p>
</div>
);
export default Checkout;
As the author of this component, you expect totalAmount
to be a number. You can't, however, be sure you actually get a number and not a string or null, so you end up guarding against garbage input. After a (short) while, this gets quite tedious and complicated.
Here's the same component with prop types applied:
import React from "react";
import PropTypes from "prop-types";
const Checkout = props => (
<div>
<p>Total sum: {props.totalAmount}</p>
</div>
);
Checkout.propTypes = {
totalAmount: PropTypes.number.isRequired
};
export default Checkout;
Now totalAmount
is declared as a number within the React ecosystem, and it's set as required, meaning React will throw an error at us if the received value is ever not a number or null or undefined. This also serves as great documentation for other developers trying to understand the component and how to use it.
If totalAmount
was not required, we'd be forced to also set defaultProps
which would explicitly state the prop's default value when the prop is not set by the caller.
import React from 'react';
import PropTypes from 'prop-types';
const Checkout = props => (
<div>
<p>Total sum: {props.totalAmount}</p>
</div>
);
Checkout.propTypes = {
totalAmount: PropTypes.number, <------ not required
};
Checkout.defaultProps = {
totalAmount: 0,
};
export default Checkout;
If totalAmount
is not set by the caller, we can now be certain its value will be 0
and not undefined
when we try to access it through props.totalAmount
.
We'll explore Prop Types more as we go along.
Oh by the way - here's you, writing React code on the next exercise!
As a footnote before moving on, we typically destructure the props
object to avoid having to write props.blah
. It just reads cleaner and gives less boilerplate code.
So instead of:
const TodoList = props => <div>{props.todoItems.length}</div>;
We'd write:
const TodoList = ({ todoItems }) => <div>{todoItems.length}</div>;
A component with many props would be formatted like this:
const TodoList = ({
todoItems,
listName,
maxNrOfItems,
onItemAdded,
onItemDeleted
}) => (
<div>
<h1>{listName}</h1>
<ul>
{todoItems.map(item => (
<li>{item.description}</li>
))}
</ul>
/* ... */
</div>
);
Object destructuring is a JavaScript language feature and not React.
📖 We'll touch on more JSX goodies as we go along, but here's a few things you'll need to know:
- We can write plain JavaScript by wrapping it in
{ }
inside of any jsx code block. We use this quite frequently to do conditional checks and write data to the screen. What this block accepts is a javascript expression, not statements. - We iterate over lists like this:
const TodoList = props => (
<ul>
{props.todoItems.map(todoItem => (
<li>{todoItem.description}</li>
))}
</ul>
);
- We do conditional checks like this:
const Checkout = props => (
<div>
{props.isOlderThanMinAgeLimit && <p>You may buy this product</p>}
{!props.isOlderThanMinAgeLimit && <p>You may NOT buy this product</p>}
</div>
);
Or like this:
const Checkout = props => {
const message = props.isOlderThanMinAgeLimit ? (
<p>You may buy this product</p>
) : (
<p>You may NOT buy this product</p>
);
return <div>{message}</div>;
};
As some of you may know, this is called a ternary operator. This syntax might look... different (cough cough ugly cough cough) but as you get used to reading it, you'll discover it's actually quite declarative and easy to reason about. It may not be pretty, but it's quite expressive.
In this workshop we'll make a simple Todo application.
- There will be an
h1
header for the name of this glorious app - There will be a sub-header with slightly emphasized text stating how many total tasks there are and how many of those are completed.
- There will be a textbox where a user can enter the description of a task
- There will be an "Add" button which will add the task to the list of existing tasks/todos.
- There will be a list of todo items. Each todo item will consist of:
- A checkbox with the description of the todo
- An delete button which will remove the todo item permanently
Above, we listed a series of demands for our application - we might call this our specification or spec.
Like most developers, we want to create this app with as little effort as possible. To make sure we spend as little time as possible being confused about our work at hand, maximize code re-use, and have a common understanding of how we're going to implement this app, let's spend a few minutes on planning how we're going to structure our React compments.
(The dotted lines represent component boundaries).
(No need to create all components right now, we'll get to that).
App
. Will contain the header text and the sub-components. Represents the root component of our app.Summary
. Will contain the total number of tasks and show how many of those are completed.AddTodo
. Will contain the textbox and Add-button.TodoList
. Will contain the list for all todo items.TodoItem
. Will contain a checkbox that marks a task as In Progress or Done, and a Delete button.
Each component will be in separate files with a .jsx
extension. I.e. the AddTodo
-component will be in a file called exactly AppTodo.jsx
.
Phew! That was too much text in one go! If you made it through without detouring to the internet, have a cookie (if you brought any)!