Build a Expense Tracker app with React Hooks and LocalStorage API

cover_image

Hooks in React have been available since the version 16.7.0-alpha. 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 any more.

Since previously, only a class component allowed you to have a local state. Using Hooks, you do not have to refactor a class component when using React or React Native into a functional component only because you want 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 that using React Hooks. Furthermore, to add real-time functionality, you are going to learn how to use localStorage API in a React application.

What are we building?

Here is a demo of how the end result of this tutorial will look like.

ss10

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 are required to have the following installed on your dev machine:

  • NodeJS above 10.x.x installed on your local machine
  • Know, how to run simple npm/yarn commands
  • JavaScript/ES6 and React basics

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 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 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 on the URL http://localhost:3000.

ss1

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 useState hook to define an initial state to a React component. Open App.js file and start by importing useState from 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 the function to update the state value fo the first. This why the second value starts with a conventional prefix fo set. Although you can make 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 function. Meaning 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. 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 any of the button to increase or decrease the count's value.

ss2

It works!

Building the Expense Tracker App

The Expense Tracker React application you are going to build 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 be going to 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 to the total amount of all expenses. Open App.js file 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, you 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 in ALL_EXPENSES array. This method executes a callback that takes two arguments 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 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 docs.

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

ss3

Add input fields

The application currently requires two input fields and a button. Both the input field 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 UI library such as reactstrap is going to be at the end of this section to serve the purpose of displaying 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.

ss4

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 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 file. 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.

ss5

Handling controlled input fields with Hooks

In this section, let us manage to convert both the static input fields which are as of right now, 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 use useState() inside App component. Both of them are going to have an empty string as their initial values.

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 purpose.

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 in 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 file and destructor the props as well as update both the input fields with attributes such as value and onChange method.

Here is how the modified Form.js component looks like.

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 Console tab from the Developer Tools. Start typing into an input field, and you will see the console statement corresponding to a particular input fields triggers.

ss6

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 name and amount input fields. Now the hard part. Right now, the expenses array has already an initial value with three individual expense objects. If you are going to add to that array, you will have to take care of not 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.

ss7

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

ss8

Adding localStorage API to persist data

Right now, there is a way to persist these values permanently since all you are using a mock array to display and add new expenses. Using localStorage() API let 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.

ss9

Adding side-effects

Using the hook useEffect you can handle lifecycle methods directly inside the 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 dependency being passed changes or gets an update, then only it will run.

Import the useEffect app from React in App.js file.

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. First is going to be a callback function and second is going to 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.

ss10

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

ss11

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 destructor 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.

ss12

On clicking the delete button will erase all the items from the localstorage.

ss13

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 can highly recommend following React official documentation here.


Originally published at Crowdbotics' Blog.


🙏  Share with your friends on Twitter

👇  Find more posts on the following topics