Create your first React app - Part 1
React is currently the most popular JavaScript library for creating web based user interfaces. It's a great technology for you to learn because there are so many job opportunities for React developers.
The name 'React' comes from the concept of 'reactivity', which means that a user interface (UI) and the data it displays are kept in sync. So, if the data changes, then the UI should automatically 'react' by displaying the updated data. Likewise, if the UI changes (for example a user changes data that is displayed in a text input), the app should 'react' by updating the underlying data.
To demonstrate reactivity, I created this sample page.
The purpose of this project is to introduce you to the most important concepts used in React. We will build a small CRUD application that allows you to manage 'users'.
It takes time and practice to learn React, and you will probably find it to be overwhelming at first. You should have many questions along the way, which I hope you will raise in our class meetings. After we finish this project, and move on to other React projects, we can revisit this one to discuss React concepts within the context of a relatively simple project.
Before getting started with React, you should be comfortable with the following JavaScript topics:
- DOM basics (adding/removing DOM objects and accessing their properties)
- Arrow functions and callback functions
- Array methods (map(), find(), etc.)
- Event handling (submit, click, change events, etc.)
- Destructuring
- Copying arrays and objects with the spread operator (...)
It also helps to understand the following topics:
This sample project uses a UserDataAccess component to do CRUD operations on the browser's localStorage database (we created this component in the Web II class). In case you did not take that class, the code is provided below. It's a simplified 'data access' component that works with the browser's localStorage database. We'll start using it in the second part of this project.
Part 1 - Setting up the app
To set up the starter files, create a folder for this project (you could name it first-react-app). Then create the following files inside the project folder:
- index.html
- main.css
- user-data-access.js
- index.js
Put this code in the index.html file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>React User Manager</title>
<link rel="stylesheet" href="main.css">
<script src="user-data-access.js"></script>
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
</head>
<body>
<div id="app"></div>
<script src="index.js" type="text/babel"></script>
</body>
</html>
Note the following about this code:
- the SCRIPT elements inside the HEAD element link to the React library, as well as the Babel library (which React makes use of).
- There is a DIV element with an ID of 'app'. Our React app will be injected into this DIV.
- There is a link to index.js, which will contain the code for our React app.
Put this code inside of main.css:
form label, input{
display:block;
}
form input[type=submit], input[type=button]{
display: inline;
}
form .validation{
color: red;
}
Here is the code for the user-data-access.js file (you created this component in Web II when you did this activity):
class UserDataAccess{
///////////////////////////////////////////////
// PRIVATE INSTANCE VARIABLES (start with #)
///////////////////////////////////////////////
// We'll use the dummyData to populate the localStorage database
#dummyData = [
{id:1, firstName:"Jane", lastName:"Doe", email:"jdoe@acme.com"},
{id:2, firstName:"Tony", lastName:"Thompsom", email:"tony@acme.com"},
{id:3, firstName:"Jesse", lastName:"Jones", email:"jesse@acme.com"}
];
//////////////////////////////////
// CONSTRUCTOR
//////////////////////////////////
constructor(){
// check to see if the localStorage db already exists.
// if not, then create it an populate it with the dummy data
if(!localStorage.getItem("userData")){
localStorage.setItem("userData", JSON.stringify(this.#dummyData));
}
}
//////////////////////////////////
// PUBLIC METHODS
//////////////////////////////////
getAllUsers(){
const str = localStorage.getItem("userData");
const users = JSON.parse(str);
return users;
}
getUserById(id){
const str = localStorage.getItem("userData");
const users = JSON.parse(str);
const user = users.find(u => u.id == id);
return user;
}
insertUser(newUser){
// Set the new user's id:
newUser.id = this.#getMaxId() + 1;
const str = localStorage.getItem("userData");
const users = JSON.parse(str);
users.push(newUser); // we really should validate newUser before adding it!
localStorage.setItem("userData", JSON.stringify(users));
}
updateUser(updatedUser){
// again, we should validate updatedUser before putting it in the database
const str = localStorage.getItem("userData");
const users = JSON.parse(str);
const indexOfUserToUpdate = users.findIndex(u => updatedUser.id == u.id);
users[indexOfUserToUpdate] = updatedUser;
localStorage.setItem("userData", JSON.stringify(users));
}
deleteUser(id){
const str = localStorage.getItem("userData");
const users = JSON.parse(str);
const indexOfUserToRemove = users.findIndex(u => id == u.id);
users.splice(indexOfUserToRemove,1);
localStorage.setItem("userData", JSON.stringify(users));
}
//////////////////////////////////
// PRIVATE METHODS
//////////////////////////////////
// Used to generate IDs when new users are inserted
#getMaxId(){
const str = localStorage.getItem("userData");
const users = JSON.parse(str);
let maxId = 0;
for(let x = 0; x < users.length; x++){
if(users[x].id > maxId){
maxId = users[x].id;
}
}
return maxId;
}
}
And, finally, we can start working on the React code by putting this into index.js:
// 1. instantiate our data access component
const uda = new UserDataAccess();
// 2. import the React functions that we'll use
const {useState, useEffect} = React
// 3. define the 'root' component
function RootComponent(){
// 4. declare the state for this component
const [users, setUsers] = useState([]); // note that we pass an empty array into useState
// 5. return JSX
return (
<div>
<h1>User Manager</h1>
<button>Add User</button>
<p>Number of users: {users.length}</p>
</div>
);
}
// TODO: put the UserList component here later
// TODO: put the UserForm component here later
// 6. initialze a React application
const app = ReactDOM.createRoot(document.querySelector("#app"))
// 7. display/render the root component
app.render(<RootComponent />);
Before we go over the code, open index.html in the browser using a web server. If you try to launch index.html from the file system it won't work. But if you use the VSCode Live Server extension, or some other web server to load the page, then you should see the UI for the root component.
The UI is made up of the JSX that is returned by the RootComponent function. React components return JSX, so we'll have to spend some time getting familiar with it in this course. But for now, don't worry about JSX.
Note that the number of users should be 0 right now. This is because the 'users' state is starting out as an empty array (we'll discuss that more below).
Now we'll go over each step in the code (you don't have to completely understand all of it right now, we are just trying to get a high level overview of React):
- Instantiate the data access component
- We are not actually using this component yet, but we will in a later step
- Import the React functions that we'll use
- React comes with many various function, most of which need to be imported
- We are using the useState() function in step 4
- We will be using the useEffect() function later in the project
- BTW - Functions that start with 'use' are known as hooks in React. Hooks in React allow you to add features/functionality to components.
- Define the root component
- React follows component based design, and to create a component you define a function (more on that as we continue)
- Component function names should start with a capital letter
- Every React app must have a 'root' component
- All of the other components that we create for this project will be a child of the root component
- Declare the state for this component
- To display data in a React component, you should declare an initialize it by using the useState() function
- useState() return an array with two elements in it, which we are destructuring into a users constant and a setUsers() function.
- The users constant is initialized to an empty array (this is the parameter passed into useState())
- If you need to make a change to the users state, you must use the the setUsers() function. Never change state directly! Always use the 'set' function to update state.
- When state is changed by calling a 'set' function, React will automatically re-render (re-display) the component UI to reflect the change in state.
- Return JSX
- React component functions return JSX, which is a data type that you have probably never used if you are new to React
- We'll explore JSX much more in this course
- JSX allows you to embed JavaScript code into the HTML markup that a component returns
- Use curly braces ({}) to add JavaScript code
- We are embedding users.length to display the number of objects in the user (state) array.
- Initialze a React application
- This code creates a React application and attaches it to the elemenet with the ID of app (in index.html you will find this DIV element).
- Display/render the root component
- To use a component function, you declare it as you would an HTML element. So, &tl;RootComponent /> will invoke your RootComponent() function, which returns the JSX that will be rendered (added to the DOM).
- The parameter that you pass into app.render() should be JSX
- All self-closing elements in JSX must include the forward slash before the closing angle bracket
Again, at this point we are trying to get a high level overview of React, so don't worry if there are many questions in your head right now. We'll be digging into the concepts much deeper as we progress through the course.
Adding event handlers to JSX
In this step we'll experiment with adding a click event handler function to the button in the root component.
Update the BUTTON element to look like this:
<button onClick={()=> console.log("button clicked!")}>Add User</button>
When you run the app, and click on the button you should see the console log say 'button clicked!'
We added an 'onClick' attribute (notice the camelCasing) and set it to an anonymous arrow function, which is our event handler function. Remember that when you embed JavaScript code in JSX, you must put it inside curly braces (rather than using quotation marks like you would in HTML when setting attribute values).
Instead of using an anonymous function, you could also define a named function. Add this function, just before the return statement in the RootComponent() function (between steps 4 and 5):
const handleAddUser = (evt) => {
console.log("button clicked!");
console.log(evt);
}
Since this will be used as an event handler function, we have the option of adding a parameter if we want to make use of the event object. We'll talk about it more after we update the BUTTON element to use the handleAddUser() function instead of an anonymous one:
<button onClick={handleAddUser}>Add User</button>
If you run the app now, it should work just like it did before, but now we are logging the 'evt' parameter. This is a click event object, just like you used in Web II, but React has 'wrapped' it into an object that has extra properties and methods. These are known as 'synthetic' event objects.
Changing state
Before we wrap up this step in the project, I want to show you how you should make changes to state. The 'users' constant is the state in the root component (we declared it by calling the useState() function). In React, you should never directly change a state variable, such as 'users'. So, this would be wrong:
// WRONG!!!!!!!!!!
users.push({firstName:"John", lastName:"Doe"})
Instead, you should call the 'set' function that gets returned by useState(). Update the handleAddUser() function to look like this:
const handleAddUser = (evt) => {
setUsers([...users, {firstName:"John", lastName:"Doe"}])
}
The call to setUsers() is a line of code that you will want to get comfortable with, since you'll see lines similar to it everywhere in React.
The array passed into setUsers() is the new version of the 'users' state. We are making a copy of the initial 'users' state and then adding the new user to the copy before passing it into setUsers(). When updating state that refers to an array, you create a copy of the current state/array, then alter the copy (we added the new user), then pass the copy into the 'set' function. This helps to keep your functions 'pure'.
Side Note:
A seasoned React developer could argue that we should be calling setUsers() by passing in an arrow function, like so:
setUsers((users) => [...users, {firstName:"John", lastName:"Doe"}])
We can discuss this approach, and when you should use it, in class. But for now we'll stay away from it.
When you run the app now, notice that when you click the button, the number of users increases. This is what makes React 'reactive'. When state is changed, React will re-render the component. To do this, React will call your component function. You can see this by adding this console log as the first line in the RootComponent() function (just before step 4)
console.log("rendering root component......");
Run the app, and you'll see that every time you click the button, the code in the event handler updates the 'users' state (by calling setUsers()). Whenever state changes, React will re-render the component by invoking the component function.
I want to show you a trick before we wrap this up. Update the JSX returned by the RootComponent() function to look like this:
<div>
<h1>User Manager</h1>
<button onClick={handleAddUser}>Add User</button>
<p>Number of users: {users.length}</p>
{JSON.stringify(users)}
</div>
We've just embedded a bit of JavaScript code so that we can see the 'users' state in the UI.
Before moving on, comment out the line of code inside the handleAddUser() function (the call to setUsers()). We don't really want that there, I was just demonstrating how you should change state by calling the 'set' function. We'll add different code to add users after we start making use of the UserDataAccess component.
React apps usually fetch data from a database, and you would usually build a 'data access' component to fetch the data. So now we'll make use of the UserDataAccess component, which fetches data from the browser's localStorage database. Fetching data for a component is considered a 'side effect' in React, and to do so you should use the useEffect() function (we imported the function, along with useState() at the top of index.js).
When you call useEffect(), you pass in a function, which React will trigger/invoke after the component renders. You also pass in a second paramter, which will disuss soon.
Add this code to the RootComponent() function, put it just after the call to useState() (just before the handleAddUsers() function):
useEffect(() => {
setUsers(uda.getAllUsers());
}, [])
Note that we are passing a function as the first paramter to useEffect(). The second parameter is an empty array. This parameter is a little more difficult to explain, but for now just note that if you pass an empty array in as the second param to useEffect(), it means that you want the first param (the function) to execute after the component initially renders.
If you run the app now, you should see the array of 'user' objects returned by calling the the getAllUsers() method of the data access component. Notice how we passed this array into setUsers().
Look in the console log and you should see that the root component actually rendered twice. The second render occurred as a result of changing the 'users' state (by calling setUsers()). Rember that whenever state changes, React will re-render the component.
Understanding the useEffect() function in React is usually pretty tricky for beginners. For now, just understand that if you need to fetch data for a component, you should use useEffect() to do it. If we didn't do so, then the RootComponent() function would not be 'pure' (but you don't really need to worry about that right now).