Timothy J. Aveni
Slides available at tja.io/hackgt5/slides
React is a declarative framework for building user interfaces in JavaScript.
The DOM lets you manipulate page content with JavaScript.
hello, world
const paragraphElement = document.getElementById('content');
paragraphElement.innerHTML = 'goodbye, then';
It can...
// create elements,
const image = document.createElement('img');
// modify attributes,
image.src = 'https://hack.gt/logo.png';
// change CSS,
image.style.border = '3px solid black';
// and restructure the page.
document.body.appendChild(image);
Anything you can imagine changing about the page's HTML or CSS can be changed with the DOM.
The DOM lets you register functions that listen for events on the web page.
image.onclick = (event) => {
alert(1);
};
With these basics, we should be able to make pretty much anything.
We just got our first client: a hip new restaurant!
The restaurant has a basic website, but the menu is out of date.
yeah ok it's ugly. but it works!
Churro: $1.20
Beef burrito: $4.00
Cheese quesadilla: $2.50
Here's the problem: our client has a breakfast menu now!
Instead of having two separate menu pages, let's add JavaScript buttons to flip between the breakfast and lunch menu.
<div id="container">
<button id="breakfastButton">Show breakfast menu</button>
<button id="lunchButton">Show lunch menu</button>
<div id="menu">
<div id="item1">Churro: $1.20</div>
<div id="item2">Beef burrito: $4.00</div>
<div id="item3">Cheese quesadilla: $2.50</div>
</div>
</div>
<script src="index.js"></script>
const breakfastButton =
document.getElementById('breakfastButton');
const lunchButton =
document.getElementById('lunchButton');
const item1 = document.getElementById('item1');
const item2 = document.getElementById('item2');
const item3 = document.getElementById('item3');
breakfastButton.onclick = (event) => {
item1.innerHTML = 'Bacon and egg burrito: $3.50';
item2.innerHTML = 'Potato and cheese burrito: $2.50';
item3.innerHTML = 'Breakfast salad: $2.00';
};
lunchButton.onclick = (event) => {
item1.innerHTML = 'Churro: $1.20';
item2.innerHTML = 'Beef burrito: $4.00';
item3.innerHTML = 'Cheese quesadilla: $2.50';
};
Cool, we did it! The client has a dynamic JavaScript™ menu.
The client is happy with our work, but after a while, they want us to make a change.
It's a pretty simple change: since the client has vegetarian customers, we should offer the user the ability to highlight which foods are vegetarian.
Now, the user can check or uncheck the veggie checkbox to get some more information about the menu.
<div id="container">
<button id="breakfastButton">Show breakfast menu</button>
<button id="lunchButton">Show lunch menu</button>
<label>
<input type="checkbox" id="vegCheck"> Highlight vegetarian options
</label>
<div id="menu">
<div id="item1">Churro: $1.20</div>
<div id="item2">Beef burrito: $4.00</div>
<div id="item3">Cheese quesadilla: $2.50</div>
</div>
</div>
<script src="index.js"></script>
const breakfastButton
= document.getElementById('breakfastButton');
const lunchButton
= document.getElementById('lunchButton');
const item1 = document.getElementById('item1');
const item2 = document.getElementById('item2');
const item3 = document.getElementById('item3');
let currentMenu = 'lunch';
breakfastButton.onclick = (event) => {
currentMenu = 'breakfast';
item1.innerHTML = 'Bacon and egg burrito: $3.50';
item2.innerHTML = 'Potato and cheese burrito: $2.50';
item3.innerHTML = 'Breakfast salad: $2.00';
};
lunchButton.onclick = (event) => {
currentMenu = 'lunch';
item1.innerHTML = 'Churro: $1.20';
item2.innerHTML = 'Beef burrito: $4.00';
item3.innerHTML = 'Cheese quesadilla: $2.50';
};
const vegCheck = document.getElementById('vegCheck');
vegCheck.onchange = (event) => {
if (vegCheck.checked) {
if (currentMenu === 'breakfast') {
item2.classList.add('highlight');
item3.classList.add('highlight');
} else if (currentMenu === 'lunch') {
item1.classList.add('highlight');
item3.classList.add('highlight');
}
} else {
item1.classList.remove('highlight');
item2.classList.remove('highlight');
item3.classList.remove('highlight');
}
};
Beautiful! We've made the client happy again.
A week passes.
The client calls us.
The client is furious.
The client sends us this screenshot, from a customer:
Beef burritos are NOT vegetarian.
Did you spot the bug?
As web applications get more and more complex, this kind of error gets harder to avoid.
Here's the problem: for every transition between two program states, you're responsible for the code that handles that transition.
currentMenu = 'breakfast', veg = false |
↔ |
currentMenu = 'breakfast', veg = true |
↕ | ↕ | |
currentMenu = 'lunch', veg = false |
↔ |
currentMenu = 'lunch', veg = true |
Let's talk theory for a second.
With four distinct program states, we're responsible for up to 4 × 3 transitions.
If we add a new state, now we're responsible for 5 × 4 transitions.
And then another... That's n × (n - 1) transitions! The transition space increases quadratically with respect to state space.
That makes for pretty buggy software.
One way to solve this problem is to rethink how we handle state transitions.
Right now, our code handles an event by mutating the DOM so that the page looks how we want it to.
We could simplify this by just deleting everything when the state changes and redrawing from scratch.
This isn't such a bad idea: this is how video games work.
We'll make the HTML simpler, since all the rendering will get done with JavaScript.
<div id="container">
<button id="breakfastButton">Show breakfast menu</button>
<button id="lunchButton">Show lunch menu</button>
<label>
<input type="checkbox" id="vegCheck"> Highlight vegetarian options
</label>
<div id="menu"></div></div>
<script src="index.js"></script>
const breakfastButton =
document.getElementById('breakfastButton');
const lunchButton = document.getElementById('lunchButton');
const vegCheck = document.getElementById('vegCheck');
const menuContainer = document.getElementById('menu');
let currentMenu = 'lunch';
let veg = false;
breakfastButton.onclick = (event) => {
currentMenu = 'breakfast';
render();
};
lunchButton.onclick = (event) => {
currentMenu = 'lunch';
render();
};
vegCheck.onchange = (event) => {
veg = vegCheck.checked;
render();
};
const render = () => {
menuContainer.innerHTML = '';
const item1 = document.createElement('div');
const item2 = document.createElement('div');
const item3 = document.createElement('div');
if (currentMenu === 'breakfast') {
item1.innerHTML = 'Bacon and egg burrito: $3.50';
item2.innerHTML = 'Potato and cheese burrito: $2.50';
item3.innerHTML = 'Breakfast salad: $2.00';
if (veg) {
item2.classList.add('highlight');
item3.classList.add('highlight');
}
} else if (currentMenu === 'lunch') {
item1.innerHTML = 'Churro: $1.20';
item2.innerHTML = 'Beef burrito: $4.00';
item3.innerHTML = 'Cheese quesadilla: $2.50';
if (veg) {
item1.classList.add('highlight');
item3.classList.add('highlight');
}
}
menuContainer.appendChild(item1);
menuContainer.appendChild(item2);
menuContainer.appendChild(item3);
};
render();
It works!
Why is this easier to code?
We insert an intermediate state — the "nuked" state — where nothing is rendered.
To get from one state to another, we go through the empty state.
Getting to the intermediate state is trivial, so we only need to write code that renders each state from scratch.
What are the problems with the nuke-and-start-over approach?
Efficiency!
The DOM is stateful.
React lets us write code with the nuke-and-start-over mindset.
We write code that takes in program state and renders from scratch.
Rather than rendering directly to the page's DOM, our render function renders to a buffer in React called the "virtual DOM".
React looks at the differences between the virtual DOM before and after the state change, and it makes only the changes necessary.
// Virtual DOM (before)
<div id="menu">
<div id="item1" class="highlight">
Churro: $1.20
</div>
<div id="item2">
Beef burrito: $4.00
</div>
<div id="item3" class="highlight">
Cheese quesadilla: $2.50
</div>
</div>
// Virtual DOM (after)
<div id="menu">
<div id="item1">
Bacon and egg burrito: $3.50
</div>
<div id="item2" class="highlight">
Potato and cheese burrito: $2.50
</div>
<div id="item3" class="highlight">
Breakfast salad: $2.00
</div>
</div>
item1.classList.remove('highlight');
item2.classList.add('highlight');
item1.innerHTML = 'Bacon and egg burrito: $3.50';
item2.innerHTML = 'Potato and cheese burrito: $2.50';
item3.innerHTML = 'Breakfast salad: $2.00';
React lets you focus on how a particular program state should look, rather than how to transition from one state to another.
React code is declarative, not imperative.
A powerful engine diffs the "before" and "after" markup behind the scenes and translates the changes into a set of DOM mutations.
React is efficient and doesn't destroy DOM state, but it has all the benefits of the nuke-and-start-over approach.
get out your laptops kiddos, we're gonna write some CODE together
We'll be working on codesandbox.io. You can work on our React app right in your browser.
You can build up as you go along, or, if you ever get stuck, there will always be an up-to-date link in the bottom-right of the slide with all of the code we've written so far.
Let's have a look at the JavaScript!
class App extends React.Component {
render() {
return (
<div className="App">
Hello, world.
</div>
);
}
}
excuse me what
what is that MONSTROSITY
what kind of JAVASCRIPT has <div> tags in it????
That, my pupil, is JSX.
JSX is an extension to JavaScript syntax that lets you include markup right in your JavaScript.
To use it, you need a JSX-to-vanilla-JavaScript translation engine, like Babel.
Babel is part of the sandbox we're using for this workshop,
so you don't have to worry about it for now. In the future,
you can set up Babel and the rest of your development environment
using the create-react-app
package on npm.
Here's what that translation looks like:
// JSX input:
<div className="App">
<p>Hello, <strong>world</strong>.</p>
</div>
// Babel output:
React.createElement(
"div",
{ className: "App" },
React.createElement(
"p",
null,
"Hello, ",
React.createElement(
"strong",
null,
"world"
),
"."
),
);
class App extends React.Component {
render() {
return (
<div className="App">
Hello, world.
</div>
);
}
}
We create a class extending React.Component
. This tells
React that we want this class to use React's rendering engine.
Every component needs a render()
method. This method
must return a string, some renderable JSX, or null if it shouldn't
render anything.
render() {
const name = 'tim';
const listElements = [
<li>apples</li>,
<li>bananas</li>,
<li>oranges</li>,
];
return (
<div className="App">
<p>Hello, <strong>world</strong>.</p>
<p>2 + 3 is {2 + 3}.</p>
<p>My name is {name.toUpperCase()}.</p>
<ul>{listElements}</ul>
</div>
);
}
JSX can contain:
Today, we'll make a simple todo-list app in React.
You'll learn how to manage program state and handle user input.
We're not limited to just one App component. We can add and nest components however we like.
To do this, we just make more classes like the App component.
One rule: component names need to start with a capital letter. That's how React tells between components and HTML tags.
Let's separate our app into a Header and a TodoList component.
// App.js
import React from 'react';
import Header from './Header';
import TodoList from './TodoList';
class App extends React.Component {
render() {
return (
<div className="App">
<Header />
<TodoList />
</div>
);
}
}
export default App;
// Header.js
import React from 'react';
class Header extends React.Component {
render() {
return (
<div className="Header">
<h1>Todos</h1>
</div>
);
}
}
export default Header;
// TodoList.js
import React from 'react';
class TodoList extends React.Component {
render() {
return (
<div className="TodoList">
<div className="Todo">
Buy apples
</div>
<div className="Todo">
Start AVL homework
</div>
<div className="Todo">
Win HackGT 5
</div>
</div>
);
}
}
export default TodoList;
To avoid repetition, it'd be nice if we could extract each todo into its own <Todo /> component.
We need to be able to pass information to the Todo component. Otherwise, every instance of the component will look the same.
In HTML, elements can have attributes and children.
<a href="https://tim.works/">
my resume!
</a>
React uses props to handle both attributes and children.
Within a React component, the this.props
object
contains every attribute present at the creation site.
this.props.children
contains an array of everything
passed as a child.
<MyComponent title="Hello!">
<SomeChild />
<SomeOtherChild />
</MyComponent>
// Inside this instance of MyComponent:
this.props.title // === 'Hello!'
this.props.children // === [<SomeChild />, <SomeOtherChild />]
Now we can wrap each todo item in a <Todo />
component.
We'll use props to tell the component what text to render.
// from TodoList.js
<div className="TodoList">
<Todo text="Buy apples" />
<Todo text="Start AVL homework" />
<Todo text="Win HackGT 5" />
</div>
// from Todo.js
render() {
return (
<div className="Todo">
{this.props.text}
</div>
);
}
When these props change, React re-renders the components efficiently.
React lets us render arrays of components.
This can help us reduce our boilerplate even further.
// from TodoList.js
render() {
const todos = [
'Buy apples',
'Start AVL homework',
'Win HackGT 5',
];
const todoComponents = [];
for (let i = 0; i < todos.length; i++) {
todoComponents.push(<Todo key={i} text={todos[i]} />);
}
return (
<div className="TodoList">
{todoComponents}
</div>
);
}
Within a component, you can't change your props. You get those from your parent.
What you can change is component state.
State is stored as a variable inside the class, and it's accessed
using this.state
.
State is a special, React-controlled object. You should never change state directly. Treat state as fully immutable.
Here's how we might store our Todos inside component state.
// from TodoList.js
class TodoList extends React.Component {
state = {
todos: [
'Buy apples',
'Start AVL homework',
'Win HackGT 5',
],
};
render() {
const todoComponents = [];
for (let i = 0; i < this.state.todos.length; i++) {
todoComponents.push(<Todo key={i} text={this.state.todos[i]} />);
}
return (
<div className="TodoList">
{todoComponents}
</div>
);
}
}
This doesn't look very interesting. We just moved our "todos" variable outside of the render function.
But React gives us a this.setState()
function
that lets us update state and tells React to re-render our
components efficiently.
state = {
a: 4,
b: 8,
c: [9, 14],
}
this.setState({
a: 7,
c: [1, 3],
});
// updated state:
state = {
a: 7,
b: 8,
c: [1, 3],
}
Let's add some code to let us check off todo list items.
We'll add a button next to each todo that lets us delete that todo.
Remember that we can never mutate state directly.
When a button is clicked, we'll make a copy of the old todos array, remove the associated todo, then set the new array in the state.
// from TodoList.js
const todoComponents = [];
for (let i = 0; i < this.state.todos.length; i++) {
const deleteFunction = () => {
// make a copy of the todos array.
const newTodos = this.state.todos.slice();
// we change the COPY, not the original.
newTodos.splice(i, 1);
this.setState({ todos: newTodos });
};
todoComponents.push(
<div>
<button className="deleteButton" onClick={deleteFunction}>X</button>
<Todo key={i} text={this.state.todos[i]} />
</div>
);
}
The delete button probably belongs in the Todo component, not in the TodoList component.
But how would that work? We can't put the todos
array
in the Todo component, because that component only renders one item.
How can we update the TodoList component's state from a button in the Todo component?
The answer is: we don't. Instead, we write a function in TodoList that handles the "delete" event, and then we give Todo that function as a prop.
The Todo component handles the button press and calls the function from its props, and that event bubbles up to TodoList where it can be handled.
// from Todo.js
<div>
<button onClick={this.props.onDelete}>X</button>
<div className="Todo">
{this.props.text}
</div>
</div>
// from TodoList.js
todoComponents.push(
<Todo
key={i}
text={this.state.todos[i]}
onDelete={deleteFunction} />
);
Let's add a button to the Header component to add a todo.
We have a similar problem as before: how do we update the state in TodoList from the Header component?
Prop callbacks only let us move data further up in the component hierarchy. We can't move information up from Header into App, and then back down into TodoList.
This happens pretty often in React applications.
The solution is to lift state up.
Component state should be high enough in the hierarchy that data only flows downwards, but no higher.
In our case, it's time to bring the todos
array into
the App component.
// from App.js
class App extends React.Component {
state = {
todos: [
'Buy apples',
'Start AVL homework',
'Win HackGT 5',
],
};
render() {
return (
<div className="App">
<Header />
<TodoList
todos={this.state.todos}
onDeleteTodo={this.onDeleteTodo} />
</div>
);
}
onDeleteTodo = (i) => {
// make a copy of the todos array.
const newTodos = this.state.todos.slice();
// we change the COPY, not the original.
newTodos.splice(i, 1);
this.setState({ todos: newTodos });
};
}
// from TodoList.js
class TodoList extends React.Component {
render() {
const todoComponents = [];
for (let i = 0; i < this.props.todos.length; i++) {
const deleteFunction = () => {
this.props.onDeleteTodo(i);
};
todoComponents.push(
<Todo
key={i}
text={this.props.todos[i]}
onDelete={deleteFunction} />
);
}
return (
<div className="TodoList">
{todoComponents}
</div>
);
}
}
Now that the state is high enough, let's make an "add todo" button.
// from App.js
<div className="App">
<Header
onAddTodo={this.onAddTodo} />
<TodoList
todos={this.state.todos}
onDeleteTodo={this.onDeleteTodo} />
</div>
// ...
onAddTodo = (text) => {
if (text === '') return;
const newTodos = this.state.todos.slice();
newTodos.push(text);
this.setState({ todos: newTodos });
};
// from Header.js
class Header extends React.Component {
render() {
return (
<div className="Header">
<h1>Todos</h1>
<button
className="addButton"
onClick={this.onAddTodo}>
Add todo
</button>
</div>
);
}
onAddTodo = () => {
this.props.onAddTodo('Call mom');
};
}
Form components are a little tricky to get right.
<input type="text" />
If we make an <input /> tag like in normal HTML, we don't have any way to access the value of the text field.
We can tell React to render the result of any JavaScript expression
inside the <input /> tag, just like with other HTML tags.
We use the value
attribute to accomplish this.
<input type="text" value={this.state.text} />
Now the text field will sets its value to
this.state.text
, updating the text field whenever the
component's state changes.
But the text field isn't configured to update
this.state
when the user changes the text, so the
text field isn't editable.
To use a form control in React, we let React component state be the single source of truth for this component.
const updateText = (event) => {
this.setState({ text: event.target.value });
};
<input type="text" value={this.state.text} onChange={updateText} />
We make a controlled component by binding the
value
prop on the <input />
tag
to the component state.
Then, we add an onChange
handler to the component so
that we can update component state whenever the field is updated.
class Header extends React.Component {
state = {
editingTodo: 'Call mom',
};
render() {
return (
<div className="Header">
<h1>Todos</h1>
<input
value={this.state.editingTodo}
onChange={this.onEditTodo} />
<button
className="addButton"
onClick={this.onAddTodo}>
Add todo
</button>
</div>
);
}
onEditTodo = (event) => {
this.setState({ editingTodo: event.target.value });
};
onAddTodo = () => {
this.props.onAddTodo(this.state.editingTodo);
this.setState({ editingTodo: '' });
};
}
The full React docs are at reactjs.org.
The main topic we didn't cover today is lifecycle hooks.
Data management getting hairy? Try Redux!
Want to use React to make a mobile app? Try React Native!
What about virtual reality? React VR is a thing!
I'm always available if you have any questions. The best way to reach me is on Facebook Messenger: m.me/timothy.j.aveni
Go call your mom!