DEV Community

Cover image for Building Your First React App with Hooks (and Git) (Part II)
Bruno da Silva
Bruno da Silva

Posted on • Updated on

Building Your First React App with Hooks (and Git) (Part II)

This tutorial is a continuation of the React tutorial Part I. If you haven't finished the previous one, I suggest you go back and complete that one first before jumping into this one.

The goal is to continue building our first React app, including now state handling, along with using Git and GitHub.

This article was initially created as a lab assignment in an intro to software engineering course I've taught at Cal Poly. A series of other articles has been published. I hope you follow along!

$$ 0 - Installing git (skip if you already have git installed)

Follow this link to install git according to your OS: https://git-scm.com/book/en/v2/Getting-Started-Installing-Git

Run git --version to make sure it is installed.

1 - Creating a git repository

Here we'll initialize your app folder to be tracked by git.  In the terminal, 'cd' to your React app folder and run git init in it. From now on, your project folder is locally tracked by Git.

Go to github.com and create a repository at GitHub. If it's your first time on GitHub, you'll need an account. Note 1: At this time, DO NOT check 'initialize this repository with a README' and DO NOT check 'Add .gitignore'. You can work on the readme and gitignore files for your repo later.

2 - Committing changes and pushing

Next, we'll commit our changes. If you're new to git, remember that there are files we never commit. For instance, the /node_modules folder of your app contains your local libs. Every developer collaborating in your project would have their own /node_modules folder managed when running their npm commands. So, we'll make sure the '/node_modules' folder is in our .gitignore file.

In the root of your app folder open this file '.gitignore' and check if there's this line:
/node_modules

If you're following this tutorial having done Part I already, you'll see the .gitignore file with this line already (it was created when you ran the npx create-react-app command from Part I). Otherwise, create the .gitignore file by yourself in the root folder and add that line.

This will make git ignore whatever you place in that file and never track what's in there.

To commit changes, we have to stage our files. So, first, stage everything in your app folder except the /node_modules (we already added it to our gitignore file). For instance, here's how you stage your /src folder:

git add src

Run git status to check what's staged and what's unstaged.  Anytime you're lost, the git status command will be helpful.

Once everything is staged, you'll commit changes. Remember that, on git, we commit locally and, only after that we can push one or more commits to a remote repository (e.g., GitHub). It's a good practice to add a commit message to communicate what represents your code changes. Even if it's obvious, place a clear and concise message since, in general, these messages can be used to understand the history of code modifications and help maintenance activities.

git commit -m 'First commit of my react app'

Now we're almost ready to push changes to your remote GitHub repo. We need first to tell your local git repository that you have a remote host. Here's the command to do it (you'll also see these instructions on your GitHub repo page once you create the repo):

git remote add origin https://github.com/your-username/your-repo-name.git

This is the only time you have to run the git remote add command.

Now, locally in our git, we'll rename our 'master' (default) branch to 'main' with the following command (GitHub already recognizes 'main' as the default branch). As a side note: technically, this is not a required step, but ethically the best choice [1] [2] [3].

git branch -M main

Then, we'll call the actual push to GitHub:

git push -u origin main

Finally, you should be able to visit your GitHub repo online. Visit github.com/your-username/you-repo-name, and you will see your commit there. 

To exercise one more useful command, on GitHub, edit the existing README.md file (or create a new one) following their web interface. If you've followed all the steps since Part I you should see a README file already pushed to GH. If for some reason you don't see one, you'll see a button "Create Readme." If there's a readme.md already in your root folder, you can click on that file and then edit it on GH interface to represent a change in the remote repo. Once you hit that button and edit or create a readme file, GitHub will generate a commit triggered by its web interface.

When you're done with it by either editing an existing readme file or creating a new one, you still won't have this commit/change in your local repo. So, go to the terminal and run git pull to bring the commit to your repo (remember that git pull executes a git fetch and a git merge at once). Now, the readme file is also local since you just synchronized your local repo with the remote repo.

3 - State (back to React)

So far, we're storing our character data in an array and passing it through as props. This is good to start, but imagine if we want to be able to delete an item from the array. With props, we have a one-way data flow, but with state we can update private data from a component.

You can think of state as any data that should be saved and modified without necessarily being added to a database - for example, adding and removing items from a shopping cart before confirming your purchase.

There are different ways to handle state in React. Since we're using React functional components, we'll use the now-famous React Hooks. Hooks were added to React in 2018 (which makes it a relatively recent feature as of 2021). It's a promising feature that makes state handling code more readable and easier to maintain. There are tons of materials online to go in-depth about it, and the official React doc is a good starting point.

To start handling state with React Hooks, we need to understand what our state is. In our case, it'll be the characters array. And we'll use the so-called useState() Hook to add some local state to the MyApp component. The useState call returns a pair: the current state value and a function that lets you update the state. You can call this function from an event handler or somewhere else (we'll do it soon).

Inside src/MyApp.js

import React, {useState} from 'react';
import Table from './Table';

function MyApp() {
   const [characters, setCharacters] = useState([  
      {
        name: 'Charlie',
        job: 'Janitor',
        // the rest of the data
      },
    ]);
}
Enter fullscreen mode Exit fullscreen mode

Our data is officially contained in the state owned by the MyApp component (rather than contained as a constant in a function). Since we want to be able to remove a character from the table, we're going to create a removeOneCharacter function on the parent MyApp component. Note that it is a nested function. Since that function, removeOneCharacter, needs the 'characters' state, they need to be in the same scope.

To retrieve the state, we'll refer to the corresponding state identifier characters. To update the state, we'll use setCharacters(). We'll filter the array based on an index that we pass through, and return the new array. The filter function is a powerful Javascript built-in function worth checking if you're not familiar with.

You must use setCharacters() to modify the state instead of trying to assign a new value directly to characters. When you call setCharacters in the component, React automatically updates the child components inside it too (that is, React re-renders the child components to update them).

Inside src/MyApp.js

import React, {useState} from 'react';
import Table from './Table';

function MyApp() {
   const [characters, setCharacters] = useState([  
      {
        name: 'Charlie',
        job: 'Janitor',
        // the rest of the data
      },
    ]);

   function removeOneCharacter (index) {
      const updated = characters.filter((character, i) => {
         return i !== index
      });
      setCharacters(updated);
   }

}
Enter fullscreen mode Exit fullscreen mode

filter does not mutate the original array but rather creates a new array after applying the filtering. And our filtering criterion is defined by a conditional statement. The conditional is testing an index vs. all the indices in the array, and returning all but the one that is passed through.

Also, note that we defined the removeOneCharacter function within the MyApp main function. With that, we can be in the right scope to refer to characters and setCharacters, and since this function will only be used within the MyApp component (which is a function by itself). Btw, we'll now see how that new function will be called.

Now, we have to pass that function through to the component and render a button next to each character that can invoke the function. First, we'll pass the removeOneCharacter function through as a prop to Table.

Inside src/MyApp.js (only showing the return -- the component render)

  return (
    <div className="container">
      <Table characterData={characters} removeCharacter={removeOneCharacter} />
    </div>
  );
Enter fullscreen mode Exit fullscreen mode

Note the added prop is removeCharacter and its value in that scope is removeOneCharacter.

Since our Table component is passing the props down to TableBody, we're going to have to pass the new prop through as well, just like we did with the character data.

Inside src/Table.js

function Table(props) {
  return (
    <table>
      <TableHeader />
      <TableBody characterData={props.characterData} removeCharacter={props.removeCharacter} />
    </table>
  );
}
Enter fullscreen mode Exit fullscreen mode

Now, here's where that index we defined in the removeOneCharacter() function comes in. In the TableBody component, we'll pass the key/index through as a parameter, so the filter function knows which item to remove. We'll create a button with an onClick property and pass the index through. If you're not very familiar with HTML, button is an HTML tag that has an attribute called onClick used to assign action when the button is clicked. The only change in the TableBody component is in the return call by adding a new column to the table that'll have a button with an action.

Inside src/Table.js (only changing inside the return of TableBody component)

<tr key={index}>
  <td>{row.name}</td>
  <td>{row.job}</td>
  <td>
    <button onClick={() => props.removeCharacter(index)}>Delete</button>
  </td>
</tr>
Enter fullscreen mode Exit fullscreen mode

Great! Now we have delete buttons on a new column and we can modify our state by deleting a character. I deleted Mac in the screenshot below.

Screenshot of the delete buttons on the page

Now you should understand how state gets initialized and how it can be modified.

Oh, and if you want to add a column label to that new column we added in the table body (see the 'Remove' label in the picture above), guess where you should change it? Go ahead and do it for aesthetic purposes.

4 - Committing changes and pushing them to GitHub

It's always a good practice to breakdown your programming sessions into small commits. So, now it's a good time to commit the changes you've made up to this step. Remember to stage your files, otherwise, there's nothing to commit. Git is tracking your changes. Run git status to check what git has to tell you. Then, run git add <files> to stage the files (you can stage them at once using regular expressions). 

Once they're staged, run git commit -m 'your commit msg.'

Finally, run git push to push your commit to GitHub.

5 - Creating a form to input data

Back to the project, now we have data stored in component state, and we can remove any item from our list of characters inside the component state. However, what if we wanted to be able to add new data to the state? In a real-world application, you'd more likely start with empty state and add to it, such as with a to-do list or a shopping cart.

Before anything else, let's remove all the hard-coded data from characters, as we'll be updating that through an input form now.

**Inside src/MyApp.js (empty state)

   const [characters, setCharacters] = useState([]);
Enter fullscreen mode Exit fullscreen mode

Now, let's go ahead and create a Form component in a new file called Form.js. We're going to set the initial state of the Form to be an object with some empty properties.

src/Form.js

import React, {useState} from 'react';

function Form() {   
   const [person, setPerson] = useState(
      {  
         name: '',
         job: '',
      }
   );

}
export default Form;
Enter fullscreen mode Exit fullscreen mode

Our goal for this form will be to update the state of Form every time a field is changed in the form, and when we submit the form, all that data will pass to the MyApp state (feeding the list of characters), which will then update the Table. Remember that when a component state changes, it triggers an update on the child components. State handling is where all the magic happens in React!

First, we'll make the function that will run every time a change is made to an input. The event will be passed through, and we'll set the state of Form to have the name and job of the inputs.

Add the following code to src/Form.js

function handleChange(event) {
  const { name, value } = event.target;
  if (name === "job")
     setPerson(
        {name: person['name'], job: value}
     );
  else
    setPerson(
        {name: value, job: person['job']}
     );   
}
Enter fullscreen mode Exit fullscreen mode

The function above should be defined inside the Form function since it sets the state defined inside the Form component. They need to be in the same scope.

Also, note that there's only one event at a time (either changing the name or the job field), so the function above will be called every time one of the fields (name or job) changes its value (i.e., when the user types in something).

Let's get this working before we move on to submitting the form. In the render (return call), let's get our two properties from state, and assign them as the values that correspond to the proper form keys, so the state (person) will be our source of truth for the form fields. We'll run the handleChange() function as the onChange of the input.

Inside src/Form.js (the return call of the Form function)

return (
    <form>
      <label htmlFor="name">Name</label>
      <input
        type="text"
        name="name"
        id="name"
        value={person.name}
        onChange={handleChange} />
      <label htmlFor="job">Job</label>
      <input
        type="text"
        name="job"
        id="job"
        value={person.job}
        onChange={handleChange} />
    </form>
); 
Enter fullscreen mode Exit fullscreen mode

In MyApp.js, we can render the form below the table. A new import for bringing in the Form component to src/MyApp.js

import Form from './Form';
Enter fullscreen mode Exit fullscreen mode

src/MyApp.js (Adding the form component after the table)

return (
  <div className="container">
    <Table characterData={characters} removeCharacter={removeOneCharacter} />
    <Form />
  </div>
);
Enter fullscreen mode Exit fullscreen mode

6 - Debugging frontend code on the browser

Some popular browsers such as Chrome and Firefox offer developer tools that allow us to debug our code on the frontend. For instance, if you're on Chrome you can either right-click on a page and select 'inspect' or you can access View -> Developer -> Developer Tools.

I recommend you install these Chrome or Firefox extensions to improve your experience in debugging React code using the browser developer tool: React dev tools by FB and the same for Firefox.

If you install this tool, you should be able to open the developer tools in the browser and monitor the Form internal state being updated every time you change the input fields (any time you type something in will trigger the onChange event).

7 - Submitting form data

Before we actually start this next step, it's already a good time to commit current changes. In projects where you collaborate with other developers, make small commits and more frequent pushes a habit. You should run here the same as you did in Step 4.

Cool. The last step is to allow us to actually submit that data and update the parent state. We'll create a function called updateList() on MyApp that will update the state by taking the existing characters and adding the new person parameter, using the ES6 spread operator.

Inside src/MyApp.js (a new function inside the MyApp function)

function updateList(person) {
  setCharacters([...characters, person]);
}
Enter fullscreen mode Exit fullscreen mode

Make sure you defined the function above as a nested function (that function goes inside the MyApp function). And let's make sure we pass that through as a parameter on Form. Note that the 'Form' capitalized is our React component.

<Form handleSubmit={updateList} />
Enter fullscreen mode Exit fullscreen mode

Now, in Form, we'll create an inner function called submitForm() that will call the prop handleSubmit, and pass the Form state through as the person parameter we defined earlier. It will also reset the state to the initial state, to clear the form after submit.

Inside src/Form.js (a new inner function)

function submitForm() {
  props.handleSubmit(person);
  setPerson({name: '', job: ''});
}
Enter fullscreen mode Exit fullscreen mode

Again, note the function above is a nested function. It should be defined inside the Form function since it uses the 'person' state that is within the Form scope.

And since we're now using handleSubmit prop (code above), we need to make it visible by adding 'props' as a parameter of the Form function.

src/Form.js (adding the props parameter)

function Form(props) { ... } //same function just showing we add a parameter 'props'
Enter fullscreen mode Exit fullscreen mode

Finally, we'll add a submit button to submit the form. We're using an onClick instead of an onSubmit since we're not using the standard submit functionality (i.e., we're not sending the form to a backend component over the network yet). The click will call the submitForm we just made.

<input type="button" value="Submit" onClick={submitForm} />
Enter fullscreen mode Exit fullscreen mode

Voila! The app is complete! We can add pairs of names and jobs to the table and delete them. Since the Table and TableBody were already pulling from the MyApp state, it will display properly.

To review what you did, take a moment to write down the resulting component hierarchy, mark what state each component handles, and in what direction data is moved around on each action (typing in the form, submitting form, and deleting rows).

If you followed the previous steps, all you have to do here is to commit and push your last changes. You will do exactly what you did in Step 4 (stage files, commit them, and push to GitHub).

If you reached this point, good job! You just completed your first React app with Hooks while following a basic git/github workflow. You used skills, tools, and practices that are valued in our industry.

If you want to discuss anything related to this content, please drop me a line on Twitter (@BrunoDaSilvaSE) or a comment below.

I welcome your feedback!

Discussion (0)