[flaviocopes] The React Course - React Bill Tracker App

React Bill Tracker App

Use React to build an app to track your bills costs. This is a more traditional application, but one that is very fulfilling to create because you’ll use it for real!

I recommend to keep those resources of mine
at hand while following the course:

Introduction to the bill tracker app

In this module, we’re going to build a bill tracker application.

Every month you get a few bills (rent, internet, electricity, gas) and our goal here is to make a cool application that keeps track of them, written using React.

When the app opens for the first time, it asks you to add your first bill category:

After you add a category name, you can add your first bill for that category:

You add a few

then you can see all the bills together as well:

You can remove bills by clicking the x next to each one.

You can also add new categories by pressing the + button on the top right, and clicking one category in the navigation filters bills by category, and re-renders the chart so you will see only the bills that belong to one.

Looks like a good set of screens to handle, and relatively not a lot complicated from the point of view of the implementation difficulty.

Let’s start!

Let’s start

As with any project in this course you can either work locally with create-react-app or use CodeSandbox.

Go to https://codesandbox.io/s and choose React to start a new CodeSandbox React project.

Let’s talk about the app structure, and define the main components that will compose our Bill Tracker application.

If you remember the screenshots in the previous lesson, we’ll need:

  1. One component to enter a new category
  2. One component to display the categories list on top
  3. One component to list the bills table
  4. One component to display the chart
  5. One component to add a new bill

Based on what they should do, let’s give those components a proper name:

  1. AddCategory
  2. NavBar
  3. BillsTable
  4. Chart
  5. AddBill

Go on and create an empty .js file for each of those components in the src/components folder (create that folder as well).

Inside each of those files, add a simple placeholder:

import React from 'react'

export default () => {
  return <div />
}

You could also just create components as you need them, but this little analysis up front gives you a more organized approach.

Lay out the components

In the previous lesson, we defined the names of the components of the app. Now we’ll add them to the App component (found in src/index.js ), and we’ll organize them inside its JSX.

Open the src/index.js file.

We first import the components we defined, so they are available for usage:

import React from "react";
import ReactDOM from "react-dom";

import "./styles.css";

import AddCategory from './components/AddCategory.js'
import AddBill from './components/AddBill.js'
import NavBar from './components/NavBar.js'
import Chart from './components/Chart.js'
import BillsTable from './components/BillsTable.js'

function App() {
  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>Start editing to see some magic happen!</h2>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Then we drop the h1 and h2 elements inside the App component JSX, and we can start drafting it to suit our application structure.

We have the NavBar on top, followed by the main part of the page, separated into 2 parts: the table, and the chart.

function App() {
  return (
    <div className="App">
      <NavBar />
      <div className="container flex">
        <div className="w-1/2">
          <BillsTable />
        </div>
        <div className="w-1/2">
          <Chart />
        </div>
      </div>
    </div>
  )
}

See those classes I added to the div container elements? They are quite clear about what the thing should do: there should be a container element div which uses Flexbox to lay out its 2 child elements. Both those child elements have a w-1/2 class assigned, which means they should each take 50% of the space available.

container , flex and w-1/2 are Tailwind.css classes.

If you’re unfamiliar with it, Tailwind is a cool CSS utility framework . Compared to Bootstrap or other popular frameworks, it does not provide any kind of widget, but instead, it provides a set of classes that do one thing, and they do it well. Check out briefly the introduction on the Tailwind site before going on.

Now, we need to include Tailwind in the project, in order for those classes to work.

How do we do it? There are different ways to include Tailwind in a project. The simplest one is to just use a link tag in your HTML, and it’s also the only way we can do it on CodeSandbox.

Open the index.html file in the public folder and add a link tag to include the Tailwind CSS:

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tailwindcss/dist/tailwind.min.css" />

Now if you add the name of the component in each component file, you will get this structure, as expected:

Add a bill category

The application will need to manage 2 different kinds of data: bills and categories .

Bills will be displayed in the BillsTable component, while categories will be shown in the NavBar component.

Despite being displayed in separate components, they will be both handled inside the App component, because that’s where we’ll centralize the data management.

Now, before being able to do anything in the app, when we start we want to show a box where the user adds the first category: the AddCategory component.

We do this by checking in the JSX if we should show that component or not. I’m going to add a property called shouldShowAddCategory to the App component state, using Hooks, and check that. We initialize it to true , for the time being.

const [shouldShowAddCategory, setShouldShowAddCategory] = useState(true)

We use conditional rendering , a technique that allows us to check some property and render something different depending on its value:

function App() {
  const [shouldShowAddCategory, setShouldShowAddCategory] = useState(true)
  return (
    <div className="App">
      {shouldShowAddCategory ? (
        <AddCategory />
      ) : (
        <div>
          <NavBar />
          <div className="container flex">
            <div className="w-1/2">
              <BillsTable />
            </div>
            <div className="w-1/2">
              <Chart />
            </div>
          </div>
        </div>
      )}
    </div>
  )
}

Let’s add a form to the AddCategory component now. We’ll make it simple, just a single input field, and a button to add the new category.

import React from 'react'

export default () => {
  return (
    <form>
      <h1>Enter a category of bills</h1>
      <p>E.g. 'Electricity' or 'Gas' or 'Internet'</p>
      <input placeholder="Add category" />
      <button>Add</button>
    </form>
  )
}

When the user enters a value in the input field, the onChange event handler is fired, and we store the new value in it:

import React, { useState } from 'react'

export default () => {
  const [category, setCategory] = useState()

  const handleChange = e => {
    setCategory(e.target.value)
  }

  return (
    <form>
      <h1>Enter a category of bills</h1>
      <p>E.g. 'Electricity' or 'Gas' or 'Internet'</p>
      <input placeholder="Add category" value={category} onChange={handleChange} />
      <button>Add</button>
    </form>
  )
}

On click of the “add” button, we add an handleSubmit event handler, which triggers the onSubmit event handler we pass as a prop by App. We need to call preventDefault() on the event object to avoid the default browser behavior that happens when we submit a form:

import React, { useState } from 'react'

export default (props) => {
  const [category, setCategory] = useState()

  const handleChange = e => {
    setCategory(e.target.value)
  }

  const handleSubmit = e => {
    e.preventDefault()
    if (!category) {
      alert('Please enter a category')
      return
    }

    props.onSubmit(category)
  }

  return (
    <form>
      <h1>Enter a category of bills</h1>
      <p>E.g. 'Electricity' or 'Gas' or 'Internet'</p>
      <input placeholder="Add category" value={category} onChange={handleChange} />
      <button onClick={handleSubmit}>Add</button>
    </form>
  )
}

in src/index.js:

<AddCategory onSubmit={addCategory} />

We use hooks to create a new state for the categories list:

const [categories, setCategories] = useState([])

and we implement the addCategory() method:

const addCategory = category => {
  const updatedCategories = [...(categories || []), category]
  setCategories(updatedCategories)
  setShouldShowAddCategory(false)
}

the [...(categories || []), category] construct allows us to create a new array from the existing categories, appending a new one. Takes care of the spread when the categories array is null or undefined, defaulting it to an empty array with || [] .

Here’s the full code of App up to now:

import React, { useState } from 'react'
import ReactDOM from 'react-dom'

import './styles.css'

import AddCategory from './components/AddCategory.js'
import AddBill from './components/AddBill.js'
import NavBar from './components/NavBar.js'
import Chart from './components/Chart.js'
import BillsTable from './components/BillsTable.js'

function App() {
  const [shouldShowAddCategory, setShouldShowAddCategory] = useState(true)
  const [categories, setCategories] = useState([])

  const addCategory = category => {
    setCategories([...categories, category])
    setShouldShowAddCategory(false)
  }

  return (
    <div className="App">
      {shouldShowAddCategory ? (
        <AddCategory onSubmit={addCategory} />
      ) : (
        <div>
          <NavBar />
          <div className="container flex">
            <div className="w-1/2">
              <BillsTable />
            </div>
            <div className="w-1/2">
              <Chart />
            </div>
          </div>
        </div>
      )}
    </div>
  )
}

const rootElement = document.getElementById('root')
ReactDOM.render(<App />, rootElement)

Now when you enter a category name, the form goes away and you see the rest of the application.

The form, however, is quite ugly! Since we use Tailwind, we just need to add some classes to the template to make it look good:

import React, { useState } from 'react'

export default props => {
  //...

  return (
    <form className="h-100 w-full flex items-center justify-center font-sans">
      <div className="bg-white rounded shadow p-6 m-4 w-full lg:w-3/4 lg:max-w-lg">
        <div className="mb-4">
          <h1 className="text-grey-darkest">Enter a category of bills</h1>
          <p>E.g. 'Electricity' or 'Gas' or 'Internet'</p>
          <div className="flex mt-4">
            <input
              className="shadow appearance-none border rounded w-full py-2 px-3 mr-4 text-grey-darker"
              placeholder="Add category"
              value={category}
              onChange={handleChange}
            />
            <button
              className="flex-no-shrink p-2 border-2 rounded bg-teal text-white border-teal hover:text-white hover:bg-teal"
              onClick={handleSubmit}
            >
              Add
            </button>
          </div>
        </div>
      </div>
    </form>
  )
}

I basically added some classes, plus an additional container div . Every class in Tailwind performs a single thing. So for example m-4 adds 1rem of margin, p-6 adds a 1.5rem padding, and so on. You are welcome to discover all those classes on the official Tailwind docs, they are not React related, but rather add some nice styling to our app.

If you think those classes clutter the HTML, you’re right, but take a look at how we didn’t add a single line of CSS to make the form look good.

You can see the app at this point in this CodeSandbox.

Storing the data

In the previous lesson, we added a form to insert a new category.

However, when we reload the page, we’re presented that form again, because the category was not persisted anywhere.

Let’s do it, and save the category to local storage.

Tip: if you are unfamiliar with local storage, check this article I wrote about it.

How do we do that? We add a locaStorage.setItem() call into the addCategory method we defined previously:

const addCategory = category => {
  const updatedCategories = [...(categories || []), category]
  setCategories(updatedCategories)
  setShouldShowAddCategory(false)
  localStorage.setItem('categories', JSON.stringify(updatedCategories))
}

In addition to storing the data, we also need to retrieve it when the application starts.

To do that, we use the Effect hook, useEffect , on the App component:

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

//...
function App() {
  //..
  useEffect(() => {
    if (localStorage.getItem('categories')) {
      setCategories(localStorage.getItem('categories'))
    }

    if (!categories.length) {
      setShouldShowAddCategory(true)
    }
  })
  //...
}

If you add a console.log() in the useEffect() callback you’ll notice this function continuously runs, ad infinitum, because we are continuously call the setShouldShowAddCategory() method, which in turns causes a rerender. The useEffect() hook is called on component mount, and on component update. We need to add a second argument to useEffect , an empty array, to only run our effect on mount:

useEffect(() => {
  const categoriesInLocalStorage = JSON.parse(
    localStorage.getItem('categories')
  )

  if (categoriesInLocalStorage !== categories) {
    setCategories(categoriesInLocalStorage)
  }

  if (!categoriesInLocalStorage) {
    setShouldShowAddCategory(true)
  }
}, [])

Now when adding a category and then you reload the app, you won’t see the AddCategory component anymore, unless you clear the local storage manually using the browser console, using this line of JavaScript:

localStorage.setItem('categories', '')

See the app in the current state on CodeSandbox.

Showing the list of categories, and adding new ones

Now that we can enter a category, when we go to the main screen, we should see it on the top of the screen, and we also want a + button to show us again the AddCategory component so we can add a new one.

Let’s do it.

First, let’s change the

const [shouldShowAddCategory, setShouldShowAddCategory] = useState(true)

to default shouldShowAddCategory to false:

const [shouldShowAddCategory, setShouldShowAddCategory] = useState(false)

Otherwise we’d always see the “add category” screen first.

From the App component, we pass the categories list to the NavBar component:

<NavBar categories={categories} />

and in NavBar.js we show them in this way:

import React from 'react'

export default props => {
  return (
    <ul>
      {props.categories
        ? props.categories.map((value, index) => {
            return <li key={index}>{value}</li>
          })
        : '<li>No categories</li>'}
    </ul>
  )
}

Next, we add the + button so we can add a new category.

import React from 'react'

export default props => {
  return (
    <ul>
      {props.categories
        ? props.categories.map((value, index) => {
            return <li key={index}>{value}</li>
          })
        : '<li>No categories</li>'}
      <li>➕</li>
    </ul>
  )
}

We emit the showAddCategory event to the parent:

import React from 'react'

export default props => {
  const triggerShowAddCategory = () => {
    props.showAddCategory()
  }

  return (
    <ul>
      {props.categories
        ? props.categories.map((value, index) => {
            return <li key={index}>{value}</li>
          })
        : '<li>No categories</li>'}
      <li onClick={triggerShowAddCategory}>➕</li>
    </ul>
  )
}

So now in src/index.js , we need to handle that:

const showAddCategory = () => {
  setShouldShowAddCategory(true)
}

and we pass it as a prop to NavBar :

<NavBar categories={categories} showAddCategory={showAddCategory} />

Cool! We can now add some Tailwind classes to make it all look good, and we’re done:

import React from 'react'

export default props => {
  const triggerShowAddCategory = () => {
    props.showAddCategory()
  }

  return (
    <ul className="list-reset inline flex justify-center border-b-4 mb-0">
      {props.categories
        ? props.categories.map((value, index) => {
            return (
              <li
                className="p-4 inline bg-grey-lighter hover:bg-grey-light uppercase font-black cursor-pointer"
                key={index}
              >
                {value}
              </li>
            )
          })
        : '<li>No categories</li>'}
      <li
        className="p-4 inline bg-grey-lighter hover:bg-grey-light uppercase font-black cursor-pointer"
        onClick={triggerShowAddCategory}
      >
        ➕
      </li>
    </ul>
  )
}

Note: I repeat some styles in the two li elements. If you use create-react-app locally, you can create a class to include those styles:

.top-menu-item {
  @apply .p-4 .inline .bg-grey-lighter  .uppercase .font-black;
}

Unfortunately, this is not possible at the moment using CodeSandbox, so for the sake of simplicity, we’ll stick with duplicates.

I tell you just to know that Tailwind can avoid being that verbose, and much more concise.

You could also just extract a string:

import React from 'react'

export default props => {
  const triggerShowAddCategory = () => {
    props.showAddCategory()
  }

  const liStyle =
    'p-4 inline bg-grey-lighter hover:bg-grey-light uppercase font-black cursor-pointer'

  return (
    <ul className="list-reset inline flex justify-center border-b-4 mb-0">
      {props.categories
        ? props.categories.map((value, index) => {
            return (
              <li className={liStyle} key={index}>
                {value}
              </li>
            )
          })
        : '<li>No categories</li>'}
      <li className={liStyle} onClick={triggerShowAddCategory}>
        ➕
      </li>
    </ul>
  )
}

See the app in the current state on CodeSandbox.

Adding bills

Now that we have a way to create categories, it’s time for handling bills.

We want to:

  1. have a way to enter a bill
  2. list all the bills

Let’s start with the first one. We’ll list bills in the next lesson.

Bills are stored in the App component, in its state bills property (an array). We already have the property in place.

Some design decisions I made:

  • for each bill, we’ll store an object into this array, with a date, a category name, and the amount.
  • the category is a string that represents the category name, not a reference to the category.
  • for convenience, all bills are expressed in $ , but you can, of course, choose your own currency symbol.

Here’s an example bill object:

{
  date: '2018-01-30T15:08:26.118Z',
  amount: '220',
  category: 'Electricity'
}

I’m going to create an AddBill component, which will be very similar to the AddCategory component, except this time we have 2 more fields. One is a date picker, and another is a select that shows the categories list.

Let’s do it! First, let’s replicate the thing we have in the AddCategory component:

import React, { useState } from 'react'

export default props => {
  const [amount, setAmount] = useState(0)

  const handleChangeAmount = e => {
    setAmount(parseInt(e.target.value), 10)
  }

  const handleSubmit = e => {
    e.preventDefault()
    if (!amount) {
      alert('Please enter an amount')
      return
    }

    props.onSubmit(amount)
  }

  return (
    <form className="h-100 w-full flex items-center justify-center font-sans">
      <div className="bg-white rounded shadow p-6 m-4 w-full lg:w-3/4 lg:max-w-lg">
        <div className="mb-4">
          <h1 className="text-grey-darkest">Enter a new bill</h1>
          <p>E.g. 'Electricity' or 'Gas' or 'Internet'</p>
          <div className="flex mt-4">
            <input
              className="shadow appearance-none border rounded w-full py-2 px-3 mr-4 text-grey-darker"
              placeholder="Add category"
              value={amount}
              onChange={handleChangeAmount}
            />

            <button
              className="flex-no-shrink p-2 border-2 rounded bg-teal text-white border-teal hover:text-white hover:bg-teal"
              onClick={handleSubmit}
            >
              Add
            </button>
          </div>
        </div>
      </div>
    </form>
  )
}

Now, as mentioned we need a select that shows the categories. Those categories must be passed by the parent component App since AddBill does not have any notion of what categories are and what they are used for.

We pass the categories array as a prop:

<select>
  {props.categories
    ? props.categories.map((value, index) => {
        return (
          <option key={index} value={value}>
            {value}
          </option>
        )
      })
    : ''}
</select>

When the select changes we need to update the selected category. By default, it’s the first category passed in the categories prop:

//...
const [category, setCategory] = useState(props.categories[0])

//...
const handleChangeCategory = e => {
  setCategory(e.target.value)
}

Now it’s the turn of the last form element: the date picker.

Datepickers are hard to write, and so as a good lazy developer I searched “react datepicker” on Google, and I found this.

All we need to do is to add react-datepicker to the CodeSandbox dependencies, or if you are doing this project locally, run

npm install react-datepicker --save

or

yarn add react-datepicker

in the command line, from the root of your project.

Now we can import the react-datepicker component, and its CSS:

import DatePicker from 'react-datepicker'
import 'react-datepicker/dist/react-datepicker.css'

Then we add the state and change event handler:

const [date, setDate] = useState(new Date())
//...
const handleChangeDate = e => {
  setDate(e.target.value)
}

here’s the JSX:

<DatePicker selected={date} onChange={handleChangeDate} />

We’re almost done! Now we just need to implement the handleSubmit method, which is triggered when we press the Add button:

const handleSubmit = e => {
  e.preventDefault()
  if (!amount) {
    alert('Please enter an amount')
    return
  }

  props.onSubmit(amount, category || props.categories[0], date)
}

we check if the amount value is filled (date and category are always initialized to a first state, so there’s no need to check for their value), and we emit the onSubmit event to the parent, with as parameter the date, the category and the amount.

Using the or operator || we default to the first category if not set.

In the App component, we map onSubmit to the addBill function:

<AddBill onSubmit={addBill} categories={categories} />

Let’s implement this method:

const addBill = (amount, category, date) => {
  const bill = {amount, category, date}
  const updatedBills = [...(bills || []), bill]
  setBills(updatedBills)
  setShouldShowAddBill(false)
  localStorage.setItem('bills', JSON.stringify(updatedBills))
}

bills and setBills come from a new hook, which is same as we did for categories:

const [bills, setBills] = useState([])

setShouldShowAddBill is the same as setShouldShowAddCategory :

const [shouldShowAddBill, setShouldShowAddBill] = useState(true)

We repeat what we did for the categories to retrieve the bills from local storage in the useEffect() callback:

useEffect(() => {
  const categoriesInLocalStorage = JSON.parse(
    localStorage.getItem('categories')
  )
  const billsInLocalStorage = JSON.parse(localStorage.getItem('bills'))

  setCategories(categoriesInLocalStorage)
  setBills(billsInLocalStorage)

  if (!categoriesInLocalStorage) {
    setShouldShowAddCategory(true)
  }
}, [])

We only show this AddBill component if the shouldShowAddBill state property is true. And this will be handled later, but let’s add the logic to show it now, so we can test it.

We need to reorganize a bit the layout of the components in the App component. We only want to show this when shouldShowAddCategory is false, and we need to add another check for shouldShowAddBill :

function App() {
  //...
  return (
    <div className="App">
      {shouldShowAddCategory ? (
        <AddCategory onSubmit={addCategory} />
      ) : shouldShowAddBill ? (
        <AddBill onSubmit={addBill} categories={categories} />
      ) : (
        <div>
          <NavBar categories={categories} showAddCategory={showAddCategory} />
          <div className="container flex">
            <div className="w-1/2">
              <BillsTable />
            </div>
            <div className="w-1/2">
              <Chart />
            </div>
          </div>
        </div>
      )}
    </div>
  )
}

You should see it working now:

and if you try adding something and pressing “Add”, the view should show this:

See the app in the current state on CodeSandbox.

Listing bills

All the bills that you add are shown in the page through the BillsTable component.

We want it to be a table, and display the contents of the bills array that’s passed down from App. So here’s what we can do:

import React from 'react'

export default props => {
  return (
    <table className="table">
      <thead className="bg-blue text-white">
        <tr>
          <th scope="col">Date</th>
          <th scope="col">Amount</th>
          <th scope="col">Category</th>
          <th scope="col" />
        </tr>
      </thead>
      <tbody>
        {props.bills.map((value, index) => {
          return (
            <tr className="p4" key={index}>
              <td>{value.date}</td>
              <td>${value.amount}</td>
              <td>{value.category}</td>
            </tr>
          )
        })}
      </tbody>
    </table>
  )
}

It’s a lot like we did to iterate on categories in the NavBar component: we use map() to repeat the tr element for every bill.

Let’s set the initial state of shouldShowAddBill to false, so we can see the bills list. First add some example bills, so you can see them in the list:

const [shouldShowAddBill, setShouldShowAddBill] = useState(false)

Next in the App JSX we pass bills as a prop to BillsTable:

<BillsTable bills={bills} />

Now the date is displayed in this format: 2019-01-30T15:08:26.118Z . This is machine-readable, not human readable. We can use the popular moment library, via the react-moment package.

Install it by adding react-moment as a CodeSandbox dependency, or using npm:

npm install moment react-moment

Once you do so, we can import it

import Moment from 'react-moment'

and you can format the date by just wrapping it in the Moment component, using the format prop:

<Moment format="MMM D YYYY">{value.date}</Moment>

The MMM D YYYY string is the format of the date, and it means print the month name followed by the day number and the year .

This is what we’ll do in BillsTable:

<tbody>
  {props.bills.map((value, index) => {
    return (
      <tr className="p4" key={index}>
        <td><Moment format="MMM D YYYY">{value.date}</Moment></td>
        <td>${value.amount}</td>
        <td>{value.category}</td>
      </tr>
    )
  })}
</tbody>

Now let’s add something to the list: we want to add a small button that shows the AddBill component so we can add bills. I chose to make it part of the table, and span across an entire table row. Add it inside the tbody tag, before you iterate over the bills:

<tr>
  <td colSpan="4">
    <button className="underline" onClick={triggerShowAddBill}>Add new</button>
  </td>
</tr>

triggerShowAddBill is a function we’re going to put inside the BillsTable component:

const triggerShowAddBill = () => {
  props.showAddBill()
}

this method calls the parent (App component) showAddBill method, which sets the shouldShowAddBill state property to true by calling setShouldShowAddBill() :

const showAddBill = () => {
  setShouldShowAddBill(true)
}

We then pass it as a prop to BillsTable :

<BillsTable bills={bills} showAddBill={showAddBill} />

When this happens, the app shows the AddBill component, because that’s how we organized our App component JSX.

Notice that by passing bills in this way, the BillsTable component will automatically refresh its displayed output when we add a new bill.

Now the app should show the bills at startup, and if you click “Add bill”, you should see the AddBill component. You can add a new bill, and you should see the list again, with the new bill added.

We add a little bit of styling to BillsTable to make it pretty, by adding Tailwind classes, and we’re good to go:

import React from 'react'
import Moment from 'react-moment'

export default props => {
  const triggerShowAddBill = () => {
    props.showAddBill()
  }

  return (
    <table className="table">
      <thead className="bg-blue text-white">
        <tr>
          <th scope="col">Date</th>
          <th scope="col">Amount</th>
          <th scope="col">Category</th>
          <th scope="col" />
        </tr>
      </thead>
      <tbody>
        <tr className="p-4 bg-blue-lighter text-center">
          <td colSpan="4">
            <button className="underline" onClick={triggerShowAddBill}>
              Add new
            </button>
          </td>
        </tr>
        {props.bills.map((value, index) => {
          return (
            <tr className="p4" key={index}>
              <td>
                <Moment format="MMM D YYYY">{value.date}</Moment>
              </td>
              <td>${value.amount}</td>
              <td>{value.category}</td>
            </tr>
          )
        })}
      </tbody>
    </table>
  )
}

See the app in the current state on CodeSandbox.

Delete a bill

When we add a bill we could make an error, so it’s handy to have the option to delete one.

We’ll do that by adding an “X” next to each line in the table.

Let’s add the markup first, in the BillsTable component, where we loop on bills, we add another td element, which contains a button with a click event handler:

{props.bills.map((value, index) => {
  return (
    <tr className="p4" key={index}>
      <td>
        <Moment format="MMM D YYYY">{value.date}</Moment>
      </td>
      <td>${value.amount}</td>
      <td>{value.category}</td>
      <td>
        <button onClick={() => removeBill(index)}>𝗫</button>
      </td>
    </tr>
  )
})}

Notice how we can pass a parameter to the method so we can tell it which bill to delete. To do so we need to use an arrow function, otherwise the removeBill() method is immediately called, since we added the parentheses.

Let’s define the removeBill method:

const removeBill = index => {
  props.removeBill(index)
}

We escalate the event to the parent, the App component so it can handle removing it from the bills list:

<BillsTable
  bills={bills}
  showAddBill={showAddBill}
  removeBill={removeBill}
/>

Here’s the method:

const removeBill = index => {
  let updatedBills = [...bills]
  updatedBills = updatedBills
    .slice(0, index)
    .concat(updatedBills.slice(index + 1, updatedBills.length))
  setBills(updatedBills)
  localStorage.setItem('bills', JSON.stringify(updatedBills))
}

See, we remove an item from bills by concatenating two portions of the array, one until the element we want to remove, and one from the next element till the end. We also update the local storage to reflect the changes.

See the app in the current state on CodeSandbox.

Add chart

Now that we’re done with the table, we can add the chart. The chart should show the bills that have been received in the last 30 days.

I like using the Chart.js charts library, and here’s a great React wrapper for it: https://github.com/jerairrest/react-chartjs-2

Add it as a dependency in CodeSandbox, and also add chart.js as a dependency.

You can also install those 2 libraries locally like we did before:

yarn add react-chartjs-2 chart.js

# or with npm:

npm install react-chartjs-2 chart.js

In src/index.js, let’s change

<Chart />

to

<Chart bills={bills} />

so the Chart component has all the things it needs to display bills (and also, when bills are updated, the chart is automatically refreshed)

Now we can focus on the src/components/Chart.js file.

Chart.js can make a wide variety of charts. We want a simple bar graph, and here’s a nice example using react-chartjs-2: https://github.com/jerairrest/react-chartjs-2/blob/master/example/src/components/bar.js

We import Bar from the library:

import { Bar } from 'react-chartjs-2';

we also import the Moment library, which we’ll use in a second:

import moment from 'moment'

Now we have some pure JavaScript calculations.

Every bar should show a month, and I want to determine what are the last 12 months, so the current line is the current month, and we have other 11 bars.

Here’s the function I came up with. months is the list of month names, and based on the current month, which we determine from the current date, I fill the orderedMonths array, and I return it.

const last12Months = () => {
  const months = [
    'January',
    'February',
    'March',
    'April',
    'May',
    'June',
    'July',
    'August',
    'September',
    'October',
    'November',
    'December'
  ]

  const today = new Date()
  const orderedMonths = []
  let month = today.getMonth() + 1

  for (let i = 0; i < 12; i++) {
    orderedMonths.push(months[month])
    month === 11 ? (month = 0) : month++
  }

  return orderedMonths
}

Hint: if the function seems complicated, try pasting it in the browser console, and call it using last12Months() , and try to change anything in the code until you’re comfortable with it. Adding a bunch of console.log() statements usually helps.

The next function, processBills , accepts an array of bills, filters out bills older than 1 year, and returns the total amount we paid each month:

const processBills = bills => {
  const oneYearAgo = moment().subtract(1, 'years')
  const months = last12Months()
  const monthsWithValues = new Array(12).fill(0)

  for (const month of monthsWithValues) {
    monthsWithValues[month] = 0
  }

  for (const bill of bills) {
    if (moment(bill.date).isSameOrBefore(oneYearAgo)) {
      continue
    }
    const monthName = moment(bill.date).format('MMMM')
    const indexOfMonth = months.indexOf(monthName)
    monthsWithValues[indexOfMonth] += parseInt(bill.amount)
  }

  return monthsWithValues
}

Again, this is just plain JavaScript, you can accept this function as working or inspect it as you wish.

Now we’re at the React component code. Here’s where we put all the things together: we use the last12Months() data for the labels, and the processBills() result for each month. We craft this in a data object as needed by the Bar component, and we feed that into it:

export default props => {
  const data = {
    labels: last12Months(),
    datasets: [
      {
        label: 'Amount',
        backgroundColor: 'rgba(255,99,132,0.2)',
        borderColor: 'rgba(255,99,132,1)',
        borderWidth: 1,
        hoverBackgroundColor: 'rgba(255,99,132,0.4)',
        hoverBorderColor: 'rgba(255,99,132,1)',
        data: processBills(props.bills)
      }
    ]
  }

  return (
    <div>
      <Bar
        data={data}
        width={100}
        height={550}
        options={{ maintainAspectRatio: false }}
      />
    </div>
  )
}

Since we pass bills as props, every time we add or remove a bill the chart automatically updates.

See the app in the current state on CodeSandbox.

Filter by category

The last piece of functionality we miss is the ability to filter bills by category.

We want to click a category name in the NavBar component, and the table and chart should only display that category’s bills.

And we’ll add an ‘All’ button that is the default and will let us go back to show all the bills instead of filtering by one category.

Let’s do it!

I’m going to first restructure the App component a little bit, by:

  • adding an activeCategory property to the state
  • creating an activeBills computed property that will only return the bills associated with the currently selected categories (or all if none is selected)
  • passing activeBills to the BillsTable and Chart components, instead of bills so they will show the filtered bills.

The first change is easy, in the App component:

const [activeCategory, setActiveCategory] = useState()

Let’s create the activeBills function. We filter the bills by activeCategory , and we sort them by date in reverse chronological order (newest first):

const activeBills = () => {
  return bills
    .filter(bill =>
      activeCategory ? bill.category === activeCategory : true
    )
    .sort((a, b) => (new Date(a.date) < new Date(b.date) ? 1 : -1))
}

Now we pass that to the child components, instead of bills :

<div className="container flex">
  <div className="w-1/2">
    <BillsTable
      bills={activeBills()}
      showAddBill={showAddBill}
      removeBill={removeBill}
    />
  </div>
  <div className="w-1/2">
    <Chart bills={activeBills()} />
  </div>
</div>

Now we’re ready to go in the NavBar component and do our edits there. But first let’s add 2 new props to NavBar: activeCategory and setNewActiveCategory

<NavBar
  categories={categories}
  showAddCategory={showAddCategory}
  activeCategory={activeCategory}
  setNewActiveCategory={setNewActiveCategory}
/>

setNewActiveCategory points to this function which simply calls setActiveCategory passing the category index:

const setNewActiveCategory = index => {
  setActiveCategory(index)
}

Let’s get to the NavBar component. Here’s the NavBar component at the moment, before any change:

import React from 'react'

export default props => {
  const triggerShowAddCategory = () => {
    props.showAddCategory()
  }

  const liStyle =
    'p-4 inline bg-grey-lighter hover:bg-grey-light uppercase font-black cursor-pointer'

  return (
    <ul className="list-reset inline flex justify-center border-b-4 mb-0">
      {props.categories
        ? props.categories.map((value, index) => {
            return (
              <li className={liStyle} key={index}>
                {value}
              </li>
            )
          })
        : '<li>No categories</li>'}
      <li className={liStyle} onClick={triggerShowAddCategory}>
        ➕
      </li>
    </ul>
  )
}

Let’s add a setNewActiveCategory() function we’ll use to call the setNewActiveCategory() prop passed by the App component:

const setNewActiveCategory = index => {
  props.setNewActiveCategory(index)
}

We’ll use that inside the categories map() to apply the class bg-grey-dark if a category is active. This class is part of Tailwind and will apply a darker background. The liStyle variable is just a string, so we concatenate a new class using + .

Also, when we click a category we call the setActiveCategory method, passing the category name:

{props.categories.map((value, index) => {
  return (
    <li
      className={
        liStyle +
        (props.activeCategory === index
          ? ' bg-grey-dark'
          : ' bg-grey-lighter')
      }
      key={index}
      onClick={() => setNewActiveCategory(index)}
    >
      {value}
    </li>
  )
})}

Before iterating on categories , let’s add an “All” button. Clciking it will reset the active category:

<li
  className={
    liStyle +
    (props.activeCategory === '' || props.activeCategory === undefined
      ? ' bg-grey-dark'
      : ' bg-grey-lighter')
  }
  onClick={() => setNewActiveCategory('')}
>
  All
</li>

Now whenever we click a category, we ask App to update the activeCategory state property, and that’s automatically passed down again to the NavBar component to let it highlight the category, and React is also updating the active bills, so BillsTable and Chart components only show the active category amounts.

See the app in the current state on CodeSandbox.

Wrapping up and coding challenges

Looks like we’re done!

I have a one idea for you to work on now, to re-enforce the things you learned in this project, and really dive into the codebase on your own.

Here are the challenges I propose for the Bill Tracker app:

  1. Implement routing. Any time the view changes, the URL should change as well
  2. Have a way to get back to the main panel when you open a modal (add bill or add category). Now there is no way to go back, you need to fill the form.