React with Hooks¶
Introduction¶
Up until the mid-2010s when React was released, web development was dominated by established frameworks like jQuery, Backbone, and MooTools. These frameworks helped solve many of the problems that developers faced in the early 2000s, including a lack of cross-browser functionality. What is even more relevant to this workshop, however, is that they made creating dynamic web-pages much easier (this is known as DOM manipulation, check out this Mozilla documentation if you’re interested in knowing more).
However, these libraries posed many problems of their own. For instance, let’s say that you wanted to create a simple to-do list that took some text from a form and added it to a list after clicking an ‘Add’ button (original CodePen here):
<body>
<form>
<input type="text" name="item" />
</form>
<div id="button">Add</div>
<ol></ol>
</body>
With jQuery, you would have to add the following JavaScript code to dynamically add text to the list:
$(document).ready(
function() {
$('#button').click(
function() {
var toAdd = $('input[name=item]').val();
$('ol').append('<li>' + toAdd + '</li>');
});
}
);
As you can see, this already looks pretty obscure with a simple add-only todo-list. Can you imagine what a jQuery app could look like with a more complicated UI? A script (or multiple) like this would need to be created for every single dynamic element created, which isn’t a very maintainable or modular solution to web pages with many possible states. Luckily, the introduction of modern web frameworks like Angular, React, and Vue have largely replaced these older libraries with a more straightforward approach to web development. For the purposes of this workshop, we will focus on React due to its popularity and extensive documentation, but many of the ideas that are mentioned in this write-up also apply to Angular and Vue.
The to-do app below will mostly follow the steps outlined in this DigitalOcean tutorial: https://www.digitalocean.com/community/tutorials/how-to-build-a-react-to-do-app-with-react-hooks.
Setup¶
First, install Node.js for your OS at nodejs.org.
Next, run the following commands in your shell to create a React app with starter code and run it in your local browser:
$ npx create-react-app react-workshop
$ cd react-workshop
$ npm start
Now, navigate to your browser of choice and enter localhost:3000
in the URL.
From here, you should see a screen with the React logo. Remove all of the
boilerplate code from src/App.js
besides the import
and export
statements.
Next, for the purposes of styling (which you can edit later), add the following to
src/App.css
:
.app {
background: #044b7a;
height: 100vh;
padding: 30px;
}
.todo-list {
background: #158ad8;
border-radius: 4px;
max-width: 400px;
padding: 5px;
}
.todo {
align-items: center;
color: #000;
background: #fff;
border: none;
display: flex;
font-size: 12px;
justify-content: space-between;
margin-bottom: 6px;
padding: 3px 10px;
}
button {
background: #158ad8;
color: #fff;
border: none;
margin-left: 2px;
margin-right: 2px;
border-radius: 2px;
}
To-Do List¶
For the remainder of the workshop, we will build a basic to-do list app using
functional React components with hooks. To show how we can use state
and
props
to build a variety of modular components, we will add
read/write/update/delete functionality to this app. From these four basic
functions, one can create large-scale systems with ease.
Note
Unless otherwise specified, the components below can be implemented in
App.js
.
How to Build a To-Do List and Read a To-Do Item¶
First, let’s build the read functionality of the app! By the end of this step,
we should have a root App
component that looks like this:
const App = () => {
const [todos, setTodos] = React.useState([
{ text: "Learn about React" },
{ text: "Meet friend for lunch" },
{ text: "Build a really cool todo app" }
]);
return (
<div className="app">
<div className="todo-list">
{todos.map((todo, index) => (
<Todo
key={index}
index={index}
todo={todo}
/>
))}
</div>
</div>
);
}
Here, we can observe that there are two very important steps to building a component: 1) setting its initial state and 2) returning the element to be rendered.
Let’s take a look at #1. In React, state is just an object that every
component uses to store information about itself. In our example, we create
a state variable called todos
that holds an array of text items
corresponding to our to-do list. It is accompanied by a function called
setTodos
that we can call at any point within the App
component to change
the list. For example, we can add a new to-do item to the end of the list by
using the following call:
setTodos([...todos, { text: "Finish the React workshop" }])
When we write const [todos, setTodos] = React.useState(/* ... */);
, all
we’re really doing is initializing a state variable along with its
corresponding setter so that we can control and change the state at any stage
of the functional component’s lifecycle. This is why it’s called a Hook; we’re
prying into the component’s data and changing it directly from within! React
Hooks drastically simplify the dynamic rendering process so that we can edit
the content of webpages in real time; no jQuery needed.
Note
Since state is local to each component, it is recommended that you only get and set a component’s state inside its declaration.
Now, let’s take a look at #2. For any React component, we will always return
one HTML element. In this case, we are returning a <div>
container that holds
an array of <Todo>
components (map
is the method that creates this array).
Since <Todo>
isn’t a native HTML element, we have to create this element
ourselves by introducing another functional component above App
called Todo
,
which will return the text for its corresponding to-do item:
const Todo = (props) => {
const { index, todo } = props;
return (
<div className="todo">
{todo.text}
</div>
);
};
Notice that this functional component has one parameter called props
,
short for ‘properties.’ This variable is simply a JavaScript object
containing all of the attributes that we pass into the component:
<Todo key={index} index={index} todo={todo} />
From the element above, we can see that there are three variables in props:
key
, index
, and todo
. However, key
is a special type of prop in
React which gives each <Todo>
element in the array a unique identity,
so it does not get passed in with the rest of the props. Thus, we can
just access the values of index
and todo
when creating our Todo
component by using the following:
const { index, todo } = props
At the end of Todo
, we just return a <div>
element containing a
string of text from the to-do item.
How to Create a To-Do Item¶
While the code above works for hard coded to-do items, we can improve on this
functionality by creating new to-do items with user input. First, we should
create a form component that takes in a user’s input and returns it from the
App
component:
const App = () => {
const [todos, setTodos] = React.useState([]);
const addTodo = (text) => {
const newTodos = [...todos, { text }];
setTodos(newTodos);
};
return (
<div className="app">
<div className="todo-list">
{todos.map((todo, index) => (
<Todo
key={index}
index={index}
todo={todo}
/>
))}
<TodoForm addTodo={addTodo} />
</div>
</div>
);
}
The TodoForm
component only passes in one prop, which is addTodo
—a
function that will add a new to-do item to the todos array. We can define
TodoForm
like so:
const TodoForm = (props) => {
const { addTodo } = props;
const [value, setValue] = React.useState("");
const handleSubmit = (e) => {
e.preventDefault();
if (!value) return;
addTodo(value);
setValue("");
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
className="input"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
</form>
);
}
In this component, the state variable value
is a string that tracks
the text added to the form via user input.
The handleSubmit
function might look a bit tricky, but it’s just a
function that changes some of the contents of the app when we type
something into the form and hit the ENTER
key. Since this function is
passed into the onSubmit
attribute, we should accept a user event e
as a parameter.
Similarly, the input
element has an onChange
attribute that accepts
a function with a parameter e
—an event that is fired when a user presses
a key, which we can use to change the input string in the text box.
How to Update a To-Do Item¶
For the updating part of this workshop, we will add functionality to the list that allows us to visually complete individual to-do items. There are many ways to design this functionality, but for the sake of simplicity, we will cross out the text when a task is complete.
To determine whether a task is complete or not, we should add a new
field called isComplete
to each element in the todos
array so that
every to-do item has the structure { text: <string>, isCompleted: <boolean> }
.
To mark an item as complete, we can create a new function in the
App
component called completeTodo
that finds an item using
its index and sets isCompleted
to true
:
const completeTodo = (index) => {
const newTodos = [...todos];
newTodos[index].isCompleted = true;
setTodos(newTodos);
};
Note
The [...todos]
syntax uses something called a ‘spread operator’,
which takes all of the elements in the todos
array and creates a
new array with these elements. In other words, this creates a copy of
todos
so that we can set the state to be this new array.
Functions can be props, too! With completeTodo
, you can now pass this
function into the Todo
component in order to style an item’s text with
a strikethrough when isCompleted
is true
:
const Todo = (props) => {
const { todo, index, completeTodo } = props
return (
<div
className="todo"
style={{ textDecoration: todo.isCompleted ? "line-through" : "" }}
>
{todo.text}
<div>
<button onClick={() => completeTodo(index)}>Complete</button>
</div>
</div>
);
}
const App = () => {
// ...
return (
<div className="app">
<div className="todo-list">
{todos.map((todo, index) => (
<Todo
key={index}
index={index}
todo={todo}
completeTodo={completeTodo}
/>
))}
<TodoForm addTodo={addTodo} />
</div>
</div>
);
}
Notice that we added a <button>
element next to the text of each item
that calls completeTodo
when clicked. When you click on the ‘Complete’
button, you should now see the corresponding task being crossed out!
How to Delete a To-Do Item¶
Lastly, we will add delete functionality to the to-do list, which will
be very similar to marking an item as complete. First, let’s create a
function called removeTodo
that finds an item by its index and
splices/removes it from the todos
state array:
const removeTodo = (index) => {
const newTodos = [...todos];
newTodos.splice(index, 1);
setTodos(newTodos);
};
Next, pass removeTodo
as a prop for the Todo
component and add a
button that calls it when clicked:
const Todo = (props) => {
const { todo, index, completeTodo, removeTodo } = props
return (
<div
className="todo"
style={{ textDecoration: todo.isCompleted ? "line-through" : "" }}
>
{todo.text}
<div>
<button onClick={() => completeTodo(index)}>Complete</button>
<button onClick={() => removeTodo(index)}>x</button>
</div>
</div>
);
}
const App = () => {
// ...
return (
<div className="app">
<div className="todo-list">
{todos.map((todo, index) => (
<Todo
key={index}
index={index}
todo={todo}
completeTodo={completeTodo}
removeTodo={removeTodo}
/>
))}
<TodoForm addTodo={addTodo} />
</div>
</div>
);
}
When you click the ‘x’ button, you should see the selected item disappear from the to-do list.
Conclusion¶
Congratulations on building your first complete React application with read, write, update, and delete functionality! If you followed every step correctly, you should have an implementation that is similar to the code here:
import React from "react";
import "./App.css";
const Todo = (props) => {
const { todo, index, completeTodo, removeTodo } = props
return (
<div
className="todo"
style={{ textDecoration: todo.isCompleted ? "line-through" : "" }}
>
{todo.text}
<div>
<button onClick={() => completeTodo(index)}>Complete</button>
<button onClick={() => removeTodo(index)}>x</button>
</div>
</div>
);
}
const TodoForm = (props) => {
const { addTodo } = props;
const [value, setValue] = React.useState("");
const handleSubmit = (e) => {
e.preventDefault();
if (!value) return;
addTodo(value);
setValue("");
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
className="input"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
</form>
);
}
const App = () => {
const [todos, setTodos] = React.useState([]);
const addTodo = (text) => {
const newTodos = [...todos, { text }];
setTodos(newTodos);
};
const completeTodo = (index) => {
const newTodos = [...todos];
newTodos[index].isCompleted = true;
setTodos(newTodos);
};
const removeTodo = (index) => {
const newTodos = [...todos];
newTodos.splice(index, 1);
setTodos(newTodos);
};
return (
<div className="app">
<div className="todo-list">
{todos.map((todo, index) => (
<Todo
key={index}
index={index}
todo={todo}
completeTodo={completeTodo}
removeTodo={removeTodo}
/>
))}
<TodoForm addTodo={addTodo} />
</div>
</div>
);
}
export default App;
If you’re interested in learning about React further, get to know some of the more advanced features in the official documentation: https://reactjs.org/docs/getting-started.html.
Licensing and Attribution¶
Copyright (c) 2021 Anthony Perez (https://github.com/anthonyaperez) <aperez01@stanford.edu>
This work, including both this document and the source code in the associated GitHub repository, is licensed under a Creative Commons Attribution 4.0 International License.
This work was initially created for a workshop at Stanford Code the Change.