Image of a computer under construction next to the React logo

Hooks in React have been available since version 16.8. They are functions that allow you to use React state and a component's lifecycle methods in a functional component. Hooks do not work with classes. If you are familiar with React, you know that the functional component has been called as a functional stateless component. Not anymore.

Previously, only a class component allowed you to have a local state. Using Hooks, you do not have to refactor a React or React Native class component into a functional component in order to introduce local state or lifecycle methods in that component. In other words, Hooks allow us to write apps in React with functional components.

In this tutorial, you are going to build a small Expense Tracker app using React Hooks. Furthermore, to add real-time functionality, you are going to learn how to use the localStorage API in a React application.

What are we building?

Here is a demo of what the end result of this tutorial will look like:

Expense tracker app with application view open in Chrome

Table of Contents

  • Requirements
  • Setup Bootstrap
  • Implementing Hooks in a React app
  • Building the Expense Tracker App
  • Add input fields
  • Add a list to display expenses
  • Handling controlled input fields
  • Handling Form submission
  • Adding localStorage API to persist data
  • Adding side-effects
  • Deleting all items from the list
  • Conclusion

Requirements

In order to follow this tutorial, you'll need the following:

  • NodeJS above 10.x.x installed on your local machine
  • Knowledge of how to run simple npm/yarn commands
  • Basic understanding of JavaScript/ES6 and React

Setup Bootstrap

Bootstrap 4 uses flexbox to handle the layout of a web app. In order to get started using Bootstrap in a React app, you have first to create a react app.

npx create-react-app react-expense-tracker

Next, install the following libraries to add Bootstrap. I am going to use reactstrap that offers built-in Bootstrap 4 components. Reactstrap does not include Bootstrap CSS, so it needs to be installed.

yarn add bootstrap reactstrap

After installing these dependencies, open the React project you created and open the file index.js. Add an import statement to include the Bootstrap CSS file.

// after other imports
import 'bootstrap/dist/css/bootstrap.min.css'

The last step is to test and verify that reactstrap components are available to the current React app. Let us add a simple button. Open the App.js file and import both Container and Button components from reactstrap. A Container is a layout component.

import React from 'react'
import { Button, Container } from 'reactstrap'

function App() {
  return (
    <Container style={{ marginTop: 20 }}>
      <Button color='success'>Let's start</Button>
    </Container>
  )
}
export default App

Now, go back to the terminal window and run yarn start. You will see similar results in the browser window at the URL http://localhost:3000.

A button displaying "Let's start" in a browser window

That's it for setting up Bootstrap 4 in a React app.

Implementing Hooks in a React App

In this section, you are going to learn how to use the useState hook to define an initial state for a React component. Open the App.js file and start by importing useState from the React core. All built-in hooks can be imported this way.

import React, { useState } from 'react'

The useState hook returns two values in an array. The first value is the current value of the state object, and the second value in the array is the function to update the state value of the first. This why the second value starts with a conventional prefix of set. You can technically name it anything, but following conventions that are commonly used in the React world is a good practice to follow.

Hooks are always called at the top level of a function. When defining a state, they must be the first thing in the function, especially before returning a JSX. Let us implement a classic example of incrementing and decrementing an initial value of 0. Inside the App function, define the following.

const [count, setCount] = useState(0)

React preserves this state between all the re-rendering happening. The useState() hook also takes a single argument that represents the initial state. Here is the code of the complete App function.

function App() {
  const [count, setCount] = useState(0)

  return (
    <Container style={{ marginTop: 20 }}>
      <p className='text-primary'>You clicked {count} times.</p>
      <Button onClick={() => setCount(count + 1)} color='success'>
        Increase the count
      </Button> <Button onClick={() => setCount(count - 1)} color='danger'>
        Decrease the count
      </Button>
    </Container>
  )
}

Make sure that yarn start is running and go the browser window to see this component in action. Click on either button to increase or decrease the count's value.

A click tracker above two buttons to increase or decrease the click count

It works!

Building the Expense Tracker App

The Expense Tracker React application you are going to build is going to contain two input fields that will contain the expense cause (or the name of the expense) and amount of that expense. It will also show the total amount of all expenses below a list of individual expenses. These are the main functionalities you have to implement first.

To get started, let us define an initial array that is going to store the value of each expense and the name or the title of the expense. Then, using this array with useState you can render the total amount of all expenses. Open App.js and define an object ALL_EXPENSES as below.

const ALL_EXPENSES = [
  { id: 1, name: 'Buy a book', amount: 20 },
  { id: 2, name: 'Buy a milk', amount: 5 },
  { id: 3, name: 'Book a flight ticket', amount: 225 }
]

The App function is going to be simple since there are now handler functions to modify or add a new expense for now. Define the state expenses with its initial value being all the expenses stored in ALL_EXPENSES. Then, using array.reduce, calculate the sum of all the expenses.

import React, { useState } from 'react'
import { Jumbotron, Container } from 'reactstrap'
import Logo from './logo.svg'

const ALL_EXPENSES = [
  { id: 1, name: 'Buy a book', amount: 20 },
  { id: 2, name: 'Buy a milk', amount: 5 },
  { id: 3, name: 'Book a flight ticket', amount: 225 }
]

function App() {
  const [expenses, setExpenses] = useState(ALL_EXPENSES)

  return (
    <Container className='text-center'>
      <Jumbotron fluid>
        <h3 className='display-6'>
          Expense Tracker React App
          <img src={Logo} style={{ width: 50, height: 50 }} alt='react-logo' />
        </h3>
        <div>
          <p>
            Total Expense:{' '}
            <span className='text-success'>
              ${' '}
              {expenses.reduce((accumulator, currentValue) => {
                return (accumulator += parseInt(currentValue.amount))
              }, 0)}
            </span>
          </p>
        </div>
      </Jumbotron>
    </Container>
  )
}

export default App

The reduce() method executes a function to output a single value from all the individual values from an array. In the current case, you have to calculate the total sum of all the amount values in the ALL_EXPENSES array. This method executes a callback that takes two arguments at once for each assigned value present in the array.

The first argument, accumulator, returns the value of the previous invocation of the callback. If the callback hasn't been invoked yet, provide an initial value (in the current scenario) such that the accumulator will be equal to it on the first iteration. On the initial run of the callback, the currentValue is going to be equal to the first value of the array. As this callback will run for each of value in the array, on the second iteration, the accumulator is equal to the currentValue of the first or initial iteration. That is going to be the first value in the array. Also, on the second iteration, the currentValue will be equal to the second value in the array. The process continues. To read more about how reduce() works, visit this MDN web doc.

Now, if you go to the browser window, you will get the following result.

Expense tracker app showing a total expense of $250

Add input fields

The application currently requires two input fields and a button. The input fields are going to represent the name of the expense and the amount of each expense. The button is going to add these expenses to the list of all expenses (which still needs to be created). Let us set up a new component inside components within a new file called Form.

import React from 'react'

import {
  Form as BTForm,
  FormGroup,
  Input,
  Label,
  Col,
  Button
} from 'reactstrap'

const Form = () => (
  <BTForm style={{ margin: 10 }}>
    <FormGroup className='row'>
      <Label for='exampleEmail' sm={2}>
        Name of Expense
      </Label>
      <Col sm={4}>
        <Input
          type='text'
          name='name'
          id='expenseName'
          placeholder='Name of expense?'
        />
      </Col>
    </FormGroup>
    <FormGroup className='row'>
      <Label for='exampleEmail' sm={2}>
        $ Amount
      </Label>
      <Col sm={4}>
        <Input
          type='number'
          name='amount'
          id='expenseAmount'
          placeholder='0.00'
        />
      </Col>
    </FormGroup>
    <Button type='submit' color='primary'>
      Add
    </Button>
  </BTForm>
)

export default Form

You will notice how helpful a UI library such as reactstrap is going to be at the end of this section to quickly display a form, rather than adding your own CSS. Also, note that both the input fields are of a different type. The name of the expense is of type text whereas the amount of the expense is type number.

Import this component in the App.js file.

// after other imports...
import Form from './components/Form'

function App() {
  const [expenses, setExpenses] = useState(ALL_EXPENSES)

  return (
    <Container>
      <Jumbotron fluid>
        <h3 className='display-6 text-center'>
          Expense Tracker React App
          <img src={Logo} style={{ width: 50, height: 50 }} alt='react-logo' />
        </h3>
        <div className='text-center'>
          <p>
            Total Expense:{' '}
            <span className='text-success'>
              ${' '}
              {expenses.reduce((accumulator, currentValue) => {
                return (accumulator += parseInt(currentValue.amount))
              }, 0)}
            </span>
          </p>
        </div>
        {*/ ADD THE BELOW LINE/*}
        <Form />
      </Jumbotron>
    </Container>
  )
}

export default App

In the browser window, you will get the following result.

Expense Tracker app with input fields

Add a list to display expenses

Let us setup another component that is going to display a list of expense items with their corresponding amount. To display the item in the list, items from the array ALL_EXPENSES are going to be used since it will serve as some mock data for now.

Create a file List.js and use UI components ListGroup and ListGroupItem to create an unordered list.

import React from 'react'
import { ListGroup, ListGroupItem } from 'reactstrap'

const List = ({ expenses }) => (
  <div>
    <ListGroup>
      {expenses.map(item => (
        <ListGroupItem key={item.id}>
          {item.name} - $ {item.amount}
        </ListGroupItem>
      ))}
    </ListGroup>
  </div>
)

export default List

Import this component in App.js. In the above snippet, you will notice that it accepts one prop: expenses. This refers to the ALL_EXPENSES array from the initial value of useState hook.

// after other impors
import List from './components/List'

function App() {
  const [expenses, setExpenses] = useState(ALL_EXPENSES)

  return (
    <Container>
      <Jumbotron fluid>
        <h3 className='display-6' className='text-center'>
          Expense Tracker React App
          <img src={Logo} style={{ width: 50, height: 50 }} alt='react-logo' />
        </h3>
        <div className='text-center'>
          <p>
            Total Expense:{' '}
            <span className='text-success'>
              ${' '}
              {expenses.reduce((accumulator, currentValue) => {
                return (accumulator += parseInt(currentValue.amount))
              }, 0)}
            </span>
          </p>
        </div>
        <Form />
        {*/ ADD THE BELOW LINE/*}
        <List expenses={expenses} />
      </Jumbotron>
    </Container>
  )
}

Visiting the browser window will yield the following list.

Expense tracker app with itemized expenses

Handling controlled input fields with Hooks

In this section, let's convert both of the static input fields, which are currently of no use, into usable controlled input fields. A controlled input field accepts its current value as a prop as well as a callback to change that value.

Of course, you are going to use Hooks to do this. Add the following initial state for name and amount using useState() inside the App component. Both of them are going to have an empty string as their initial value.

const [name, setName] = useState('')
const [amount, setAmount] = useState('')

To update their values when a user starts typing, add the following handler methods. Both of these functions are going to retrieve the value from the corresponding field. The console statements are for testing purposes.

const handleName = event => {
  console.log('Name ', event.target.value)
  setName(event.target.value)
}

const handleAmount = event => {
  console.log('Amount ', event.target.value)
  setAmount(event.target.value)
}

Lastly, to submit the form, there is going to be another handler method called handleSubmitForm.

const handleSubmitForm = event => {
  event.preventDefault()
  // do something when submitting the form
}

Right now, it doesn't have business logic to add the expense to the list. It is just preventing the form from refreshing the whole page upon submission using event.preventDefault().

All of these have to be passed as props to the Form component. Modify it.

<Form
  name={name}
  amount={amount}
  handleName={handleName}
  handleAmount={handleAmount}
  handleSubmitForm={handleSubmitForm}
/>

Next, open Form.js, destructure the props, and update both the input fields with attributes such as value and the onChange method.

Here is how the modified Form.js component looks.

import React from 'react'

import {
  Form as BTForm,
  FormGroup,
  Input,
  Label,
  Col,
  Button
} from 'reactstrap'

const Form = ({ name, amount, handleName, handleAmount, handleSubmitForm }) => (
  <BTForm style={{ margin: 10 }} onSubmit={handleSubmitForm}>
    <FormGroup className='row'>
      <Label for='exampleEmail' sm={2}>
        Name of Expense
      </Label>
      <Col sm={4}>
        <Input
          type='text'
          name='name'
          id='expenseName'
          placeholder='Name of expense?'
          value={name}
          onChange={handleName}
        />
      </Col>
    </FormGroup>
    <FormGroup className='row'>
      <Label for='exampleEmail' sm={2}>
        $ Amount
      </Label>
      <Col sm={4}>
        <Input
          type='number'
          name='amount'
          id='expenseAmount'
          placeholder='0.00'
          value={amount}
          onChange={handleAmount}
        />
      </Col>
    </FormGroup>
    <Button type='submit' color='primary'>
      Add
    </Button>
  </BTForm>
)

export default Form

Now, go to the browser window. Make sure to open the Console tab inside Developer Tools. Start typing into an input field, and you will see the console statement corresponding to a particular input field's triggers.

Developer tools console logging key inputs

Handling Form submission

In this section, you are going to add the logic to handle the form submission. Start by adding an if/else statement to check whether the first input field name is not empty, and the second input field amount is not a negative value.

Next, create a single expense object that takes the current value of the name and amount input fields. Now the hard part. Right now, the expenses array already has an initial value with three individual expense objects. If you are going to add to that array, you will have to take care not to override the previous expense objects in that array.

Spread operator to the rescue.

const handleSubmitForm = event => {
  event.preventDefault()
  //check whether the name is not empty and the amount is not negative
  if (name !== '' && amount > 0) {
    // single expense object
    const expense = { name, amount }
    // do not override previous values in the array
    // use spread operator to access previous values
    setExpenses([...expenses, expense])

    // clean input fields
    setName('')
    setAmount('')
  } else {
    console.log('Invalid expense name or the amount')
  }
}

Lastly, you have to clear both the input fields after the form submission. Set them back to their initial values, that is, empty strings.

Go the browser window and try adding a few items. Do notice that the Total Expense gets an update after each form submission.

Expense tracker app listing expenses upon successful form submission

On the empty submission, it will trigger the else clause. To see it in action, make sure you have Console tab in Developer Tools open.

Console showing error message for invalid entry

Adding localStorage API to persist data

Right now, there is now way for these values to persist permanently because you are using a mock array to display and add new expenses. Using the localStorage() API lets us add the functionality to save all the expenses that the user adds to the list.

The localStorage API allows you to access a Storage object that is the stored data saved across browser sessions.

Each expense value you are going to store in the localStorage API is going to be a string, so make sure you understand the difference between JSON.stringify() and JSON.parse().

Replace the current mock ALL_EXPENSES with the following conditional operator.

const ALL_EXPENSES = localStorage.getItem('expenses')
  ? JSON.parse(localStorage.getItem('expenses'))
  : []

Using the method getItem() from the localStorage API, you can read any value stored. However, right now, there is no value stored, so it is going to be an empty array. You can verify this by opening Developer Tools > Application > Storage > LocalStorage > https://localhost:3000.

Empty local storage cache in developer tools

Adding side-effects

Using the hook useEffect you can handle lifecycle methods directly inside functional components. By default, it runs after every render including the initial render, but you can control that behavior by passing dependencies in an array. If a dependency being passed changes or gets an update, then only it will run.

Import the useEffect Hook from the React module in App.js.

import React, { useState, useEffect } from 'react'

Inside this useEffect function you are going to use localStorage.setItem() to store the expenses. It accepts two parameters. The first is going to be a callback function and the second is going to be the dependency.

This dependency is going to be the expenses from the state. Basically, you are saying that, whenever there is an update to the initial value of the expenses, run the useEffect method.

Add the following after other handler methods.

useEffect(() => {
  localStorage.setItem('expenses', JSON.stringify(expenses))
}, [expenses])

Make sure the key (expenses) you are passing in setItem() is the same as the key whose value you are getting using getItem.

The useEffect is still going to run after the initial render but won't run after that until there is a change in the value of expenses.

See the demo below.

Local storage now updates upon form submission

Notice that it works. If you refresh the browser window, the list stays as it is.

The list is unaffected by a browser refresh

Deleting all items from the list

This is a small section in which you are going to add the functionality of clearing the whole list of expenses with a single button click. To do so, create a handler method and inside it, set the initial value of the expenses to an empty array.

Open App.js file and add this:

const handleClearExpenses = () => {
  setExpenses([])
}

Pass it as a prop to the Form component.

<Form
  name={name}
  amount={amount}
  handleName={handleName}
  handleAmount={handleAmount}
  handleSubmitForm={handleSubmitForm}
  handleClearExpenses={handleClearExpenses}
/>

Next, edit the Form.js file and add a new button to delete the list of items. Do not forget to destructure the new prop handleClearExpenses.

import React from 'react'

import {
  Form as BTForm,
  FormGroup,
  Input,
  Label,
  Col,
  Button
} from 'reactstrap'

const Form = ({
  name,
  amount,
  handleName,
  handleAmount,
  handleSubmitForm,
  handleClearExpenses
}) => (
  <BTForm style={{ margin: 10 }} onSubmit={handleSubmitForm}>
    <FormGroup className='row'>
      <Label for='exampleEmail' sm={2}>
        Name of Expense
      </Label>
      <Col sm={4}>
        <Input
          type='text'
          name='name'
          id='expenseName'
          placeholder='Name of expense?'
          value={name}
          onChange={handleName}
        />
      </Col>
    </FormGroup>
    <FormGroup className='row'>
      <Label for='exampleEmail' sm={2}>
        $ Amount
      </Label>
      <Col sm={4}>
        <Input
          type='number'
          name='amount'
          id='expenseAmount'
          placeholder='0.00'
          value={amount}
          onChange={handleAmount}
        />
      </Col>
    </FormGroup>
    <Button type='submit' color='primary'>
      Add
    </Button>{' '}
    <Button type='submit' color='danger' onClick={handleClearExpenses}>
      Delete
    </Button>
  </BTForm>
)

export default Form

Notice, that right now there are three items in the list, as shown below.

List with three expenses in local storage

Clicking the delete button will erase all the items from the localStorage.

Local storage deleted upon clicking delete button

Conclusion

Congratulations 🎉

You have just learned the basics of React hooks and how to implement them in a real-time application. Also, using localStorage API is easy. I hope you had fun and gained something useful out of this tutorial. Go ahead and try to extend this app by adding features like:

  • editing a single item in the list
  • deleting a single item in the list
  • adding a uniquely generated id for each item

To learn more about React hooks, I highly recommend following React's official documentation here.