How to Build a Budget App With React, Typescript & Web Storage API Pt.1

How to Build a Budget App With React, Typescript & Web Storage API Pt.1
Illustration of cash and wallet and smartphone as a metaphor for budget app.

Table of Contents

Do you want to learn React, TypeScript or Web Storage API? This tutorial will help you with it. Step by step, it will help you build your own budget app using these technologies. Learn React, TypeScript and Web Storage API while building your own budget app!

How to Build a Budget App With React, Typescript & Web Storage API Part 2.

You can find the code on my GitHub (make sure you are on “blog-tutorial” branch).

Introduction

At the end of this tutorial you will have a working budget app with following features. First, it will allow you to set a budget. You will also be able to choose in what currency you want the budget to be. Don’t worry. You don’t have to remember any codes. App will help you choose the currency code from options provided by datalist.

Second, it will allow you to choose a budget period-daily, monthly or yearly budget. Third, it will allow you to create a list of items, things, you either will buy or you already bought. Then, depending on the payment status, paid or not paid, it will show you how much budget remains. Or, if you are already in red numbers, if you’ve spent your entire budget.

Lastly, it will also allow you to store your data, items on the list and app settings, in app state or in localStorage or sessionStorage, using Web Storage API. Thanks to web storage API, you will be able to keep your data even if refresh your app in the browser. Well, only if you decide to use local or session storage as your preferred storage method. Otherwise, it will be erased.

As I mentioned, the tech stack for this budget app will be React, TypeScript and Web Storage API. In addition you will also use React Router. You will use this library to create routing for homepage and Settings page of your budget app. That’s for the introduction. Now, let’s get to work.

Project setup

The first thing you will need to do is putting together the workflow, to compile all React and CSS files. You can handle this using your own bundler such as Webpack or Parcel, and config. Simpler and easier option is to use ready-to-use boilerplate app provided by create-react-app package.

Using the create-react-app boilerplate is easy. You can use it with npm, using npm init react-app budget-app-ts --typescript or npx create-react-app budget-app-ts --typescript. Or for yarn, yarn create react-app budget-app-ts --typescript. If you don’t want to use TypeScript, omit the --typescript flag at the end of the command.

Next, you will need to install two additional packages. The first one is react-router-dom. You will use this for routing between pages in your budget app. In additional, you should also install types for this package, @types/react-router-dom. The second package is shortid, and types for it, @types/shortid.

You will use the shortid package to generate unique ids for every item on the list in your budget app. This is much better than using indexes, which is a very bad practice. It is also much easier than writing some id generator, or creating these ids manually. This is all you will need. Now, your package.json should look similar to this:

{
  "name": "budget-app-ts",
  "version": "1.0.0",
  "description": "Minimal budget app built with React & TypeScript.",
  "license": "MIT",
  "private": false,
  "browserslist": [
    ">0.2%",
    "not dead",
    "not ie <= 11",
    "not op_mini all"
  ],
  "main": "src/index.tsx",
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  },
  "dependencies": {
    "react": "16.12.0",
    "react-dom": "16.12.0",
    "react-router-dom": "5.1.2",
    "shortid": "2.2.15"
  },
  "devDependencies": {
    "@types/react": "16.9.11",
    "@types/react-dom": "16.9.4",
    "@types/react-router-dom": "5.1.2",
    "@types/shortid": "0.0.29",
    "react-scripts": "3.2.0",
    "typescript": "3.7.2"
  }
}

TypeScript

For TypeScript, let’s keep it simple and use the tsconfig.json generated by create-react-app. There is no need to change anything, unless you want to. The tsconfig.json for this project will look like this:

{
    "include": [
        "./src/*"
    ],
    "compilerOptions": {
        "lib": [
            "dom",
            "es2015"
        ],
        "jsx": "react",
        "target": "es5",
        "allowJs": true,
        "skipLibCheck": true,
        "esModuleInterop": true,
        "allowSyntheticDefaultImports": true,
        "strict": true,
        "forceConsistentCasingInFileNames": true,
        "module": "esnext",
        "moduleResolution": "node",
        "resolveJsonModule": true,
        "isolatedModules": true,
        "noEmit": true
    }
}

Project structure

Below is the final structure of this budget app project. Use this as your map as you work on this tutorial. It will help you orient yourself. It will also help you ensure you have all files you need, and at the right place. Now, let’s start working on your budget app.

budget-app-ts/
├─node_modules
├─public
│ ├─favicon.ico
│ ├─index.html
│ ├─manifest.json
│ └─robots.txt
├─src
│ ├─components
│ │ ├─item-item-add.tsx
│ │ ├─item-item.tsx
│ │ ├─item-list.tsx
│ │ ├─item-total.tsx
│ │ ├─icon-bin.tsx
│ │ └─icon-settings.tsx
│ ├─data
│ │ └─currency-codes.ts
│ ├─pages
│ │ └─home.tsx
│ │ └─settings.tsx
│ ├─styles
│ │ └─styles.css
│ ├─app-router.tsx
│ ├─index.tsx
│ ├─interfaces.ts
│ └─react-app-env.d.ts
├─ package.json
└─ tsconfig.json

Interfaces

As the first thing, let’s create interfaces for your budget app. It is better to do this now, for at least two reasons. First, it will help you plan and outline better the functionality of your components. Second, you will not run into issues with TypeScript complaining about missing types for props, etc.

Your budget app will need seven interface objects. These interface will be for BudgetItemObj object, BudgetList, BudgetItem, BudgetTotal, HomePage, SettingsPage and BudgetItemAdd component. The BudgetItemObjInterface will define the shape of on item on the list in your budget app.

Every item will contain date (date of payment), isPaid (if item has been paid), price (price of the item), title (title of the item) and id (unique id). The BudgetListInterface will contain budgetCurrency, budgetItems (array of BudgetItemObjInterface) and two handlers, handleItemUpdate and handleItemRemove.

The interface for budgetItem component will contain budgetCurrency, budgetItem and two handlers, handleItemUpdate and handleItemRemove. This is similar to the BudgetListInterface because you will pass many of the props of budgetItem component through the BudgetList component.

Next is BudgetTotalInterface. This interface will contain budgetPeriod, budgetAmount, budgetPaid, budgetCurrency. Almost all these props will come from app settings. Interfaces for pages will be also very similar. For homepage (HomePageInterface), budgetItems, budgetAmount, budgetPeriod, budgetCurrency, storageMethod and setBudgetItems hook dispatcher.

For Settings page (SettingsPageInterface), budgetAmount, budgetPeriod, budgetCurrency, storageMethod and setBudgetPeriod, setBudgetCurrency, setBudgetAmount, setStorageMethod hook dispatchers. The last one is BudgetItemAddInterface.

This interface will be very simple. It will contain showAddItem, handleAddItem handler and handleShowAddItem hook dispatcher. When you add types for every prop, handler and hook dispatcher in each interface, this is what you get:

// Interface for BudgetItemObj object
export interface BudgetItemObjInterface {
  date: string;
  isPaid: boolean;
  price: number;
  title: string;
  id: string;
}

// Interface for BudgetList component
export interface BudgetListInterface {
  budgetCurrency: string;
  budgetItems: BudgetItemObjInterface[]
  handleItemUpdate: (value: string, id: string, itemProperty: string) => void;
  handleItemRemove: (id: string) => void;
}

// Interface for BudgetItem component
export interface BudgetItemInterface {
  budgetCurrency: string;
  budgetItem: BudgetItemObjInterface;
  handleItemUpdate: (value: string, id: string, itemProperty: string) => void;
  handleItemRemove: (id: string) => void;
}

// Interface for BudgetTotal component
export interface BudgetTotalInterface {
  budgetPeriod: string;
  budgetAmount: number;
  budgetPaid: number;
  budgetCurrency: string;
}

// Interface for Homepage
export interface HomePageInterface {
  budgetItems: BudgetItemObjInterface[];
  budgetAmount: number;
  budgetPeriod: string;
  budgetCurrency: string;
  storageMethod: string;
  setBudgetItems: React.Dispatch<React.SetStateAction<BudgetItemObjInterface[]>>;
}

// Interface for Settings page
export interface SettingsPageInterface {
  budgetAmount: number;
  budgetPeriod: string;
  budgetCurrency: string;
  storageMethod: string;
  setBudgetPeriod: React.Dispatch<React.SetStateAction<string>>;
  setBudgetCurrency: React.Dispatch<React.SetStateAction<string>>;
  setBudgetAmount: React.Dispatch<React.SetStateAction<number>>;
  setStorageMethod: React.Dispatch<React.SetStateAction<string>>;
}

// Interface for BudgetItemAdd component
export interface BudgetItemAddInterface {
  showAddItem: boolean;
  handleAddItem: (payload: BudgetItemObjInterface) => void;
  handleShowAddItem: React.Dispatch<React.SetStateAction<boolean>>;
}

Icons

Let’s create components for two icons you will use in your budget app. One icon will be for removing item from the list and the second for link to Settings page. Icon for removing will be a recycle bin. Icon for Settings link will be cog, or gear. Both components will use SVG to render the icons.

First, let’s create the IconBin component:

// Import dependencies
import * as React from 'react'

// IconBin component
const IconBin = () => (
  <svg xmlns="http://www.w3.org/2000/svg" width="18" id="Layer_41" data-name="Layer 41" viewBox="0 0 50 50"><defs/><defs/><path d="M44 10h-9V8.6A6.6 6.6 0 0028.4 2h-6.8A6.6 6.6 0 0015 8.6V10H6a2 2 0 000 4h3v27.4a6.6 6.6 0 006.6 6.6h18.8a6.6 6.6 0 006.6-6.6V14h3a2 2 0 000-4zM19 8.6A2.6 2.6 0 0121.6 6h6.8A2.6 2.6 0 0131 8.6V10H19V8.6zm18 32.8a2.6 2.6 0 01-2.6 2.6H15.6a2.6 2.6 0 01-2.6-2.6V14h24v27.4z" className="cls-1"/><path d="M20 18.5a2 2 0 00-2 2v18a2 2 0 004 0v-18a2 2 0 00-2-2zM30 18.5a2 2 0 00-2 2v18a2 2 0 104 0v-18a2 2 0 00-2-2z" className="cls-1"/></svg>
)

export default IconBin

Next, the IconSettings component.

// Import dependencies
import * as React from 'react'

// IconSettings component
const IconSettings = () => (
  <svg xmlns="http://www.w3.org/2000/svg" width="21" viewBox="0 0 896 1024"><defs/><path d="M447.938 350C358.531 350 286 422.531 286 512c0 89.375 72.531 162.062 161.938 162.062 89.438 0 161.438-72.688 161.438-162.062-.001-89.469-72.001-162-161.438-162zm324.687 255.062l-29.188 70.312 52.062 102.25 6.875 13.5-72.188 72.188-118.436-55.937-70.312 28.875L505.75 945.5l-4.562 14.5H399.156L355 836.688l-70.312-29-102.404 51.938-13.5 6.75-72.156-72.125 55.875-118.5-28.969-70.25-109.065-35.626L0 565.188V463.219L123.406 419l28.969-70.188-51.906-102.469-6.844-13.438 72.062-72.062 118.594 55.844 70.219-29.031 35.656-109.188L394.75 64h102l44.188 123.469 70.125 29.031L713.5 164.531l13.625-6.844 72.125 72.062-55.875 118.406L772.25 418.5l109.375 35.656L896 458.75v101.938l-123.375 44.374z"/></svg>
)

export default IconSettings

Currency codes

Before moving any further, let’s take care of another thing you will need. This will be array of currency codes. As I mentioned, the app will allow you to choose in what currency you want the budget to be. In order to make this as easy as possible, you will use input element along with a datalist.

Why datalist and not select? There are currently around 167 currency codes. Imagine looking for one specific code in a select with 167 options. That would be insane. datalist makes it easier because it helps you narrow the selection of options as you write. One or two characters and from 167 options, there are only two or one left.

That said, you still need the data, the currency codes, for the datalist. So, let’s store it in an array, in a separate file, and export it. After that, you can import this dataset, later when you will work on Settings page. There, you will loop over it using map() and generate option element for each code.

const currencyCodes = [
  'AED', 'ALL', 'AMD', 'ANG', 'AOA', 'ARS', 'AUD', 'AWG', 'AZN', 'BAM', 'BBD', 'BDT', 'BGN', 'BHD', 'BIF', 'BMD', 'BND', 'BOB', 'BOV', 'BRL', 'BSD', 'BTN', 'BWP', 'BYN', 'BZD', 'CAD', 'CDF', 'CLF', 'CLP', 'CNY', 'COP', 'COU', 'CRC', 'CUC', 'CUP', 'CVE', 'CZK', 'DJF', 'DKK', 'DOP', 'DZD', 'EGP', 'ERN', 'ETB', 'EUR', 'FJD', 'FKP', 'GBP', 'GEL', 'GHS', 'GIP', 'GMD', 'GNF', 'GTQ', 'GYD', 'HKD', 'HNL', 'HRK', 'HTG', 'HUF', 'CHE', 'CHF', 'CHW', 'IDR', 'ILS', 'INR', 'IQD', 'IRR', 'ISK', 'JMD', 'JOD', 'JPY', 'KES', 'KGS', 'KHR', 'KMF', 'KPW', 'KRW', 'KWD', 'KYD', 'KZT', 'LAK', 'LBP', 'LKR', 'LRD', 'LSL', 'LYD', 'MAD', 'MDL', 'MGA', 'MKD', 'MMK', 'MNT', 'MOP', 'MRU', 'MUR', 'MVR', 'MWK', 'MXN', 'MXV', 'MYR', 'MZN', 'NAD', 'NGN', 'NIO', 'NOK', 'NPR', 'NZD', 'OMR', 'PAB', 'PEN', 'PGK', 'PHP', 'PKR', 'PLN', 'PYG', 'QAR', 'RON', 'RSD', 'RUB', 'RWF', 'SAR', 'SBD', 'SCR', 'SDG', 'SEK', 'SGD', 'SHP', 'SLL', 'SOS', 'SRD', 'SSP', 'STN', 'SVC', 'SYP', 'SZL', 'THB', 'TJS', 'TMT', 'TND', 'TOP', 'TRY', 'TTD', 'TWD', 'TZS', 'UAH', 'UGX', 'USD', 'USN', 'UYI', 'UYU', 'UZS', 'VEF', 'VND', 'VUV', 'WST', 'XAF', 'XCD', 'XDR', 'XOF', 'XPF', 'XSU', 'XUA', 'YER', 'ZAR', 'ZMW', 'ZWL', 'AFN'
]

export default currencyCodes

BudgetItem component

Now, let’s create the BudgetItem component. You will use this component to render individual items on the list in your budget app. This component will not contain any logic. It will only accept some props and render the markup.

The markup for BudgetItem component will be following. There will be a checkbox to mark the item as paid or not. This budget app will lower your budget, subtract the price of the item from the total budget, only when the item is paid. Next will be title of the item, followed by the date the item was, or will be, paid.

This will be followed by price and button to remove the item from the list. This, BudgetItem, component will get all this data from props. These props will come from BudgetList. Here, you will iterate over array of all items and render BudgetItem component for each. Now, comes one interesting thing.

This budget app will allow you to edit all items on the list. You will not have to open some new page or modal to edit any item. You will be able to do this right there on the list. This way you will be able to edit the title, price and the payment date, and also check it off as paid or uncheck it as unpaid.

In order to implement this BudgetItem component will render all data, title, price, etc., through inputs. You will use text inputs for title and price, checkbox input (with custom styles) for marking the item as paid and date input for date of payment. Each of these inputs will also have a onChange event handler, handleItemUpdate function.

This handler function will be passed through props. It will accept three parameters, value passed to the input, type of the data (title, price, is item paid) and id of the item. The id will guarantee every change will be made only to one specific item.

// Import dependencies
import * as React from 'react'

// Import interface
import { BudgetItemInterface } from './../interfaces'

// Import components
import IconBin from './icon-bin'

const BudgetItem = (props: BudgetItemInterface) => {
  return (
    <div className="budget-item">
      <div className="budget-item-paid">
        {/* Checkbox to mark the item as paid */}
        <input
          className="custom-checkbox-checkbox"
          type="checkbox"
          id={props.budgetItem.id}
          checked={props.budgetItem.isPaid}
          onChange={(event) => props.handleItemUpdate(event.target.value, props.budgetItem.id, 'isPaid')}
        />

        <label className="custom-checkbox-label" htmlFor={props.budgetItem.id} />
      </div>

      <div className="budget-item-title">
        {/* Title of the item */}
        <input
          type="text"
          value={props.budgetItem.title}
          onChange={(event) => props.handleItemUpdate(event.target.value, props.budgetItem.id, 'title')}
        />
      </div>

      <div className="budget-item-date">
        {/* Date the item was added */}
        <input
          type="date"
          value={props.budgetItem.date}
          onChange={(event) => props.handleItemUpdate(event.target.value, props.budgetItem.id, 'date')}
        />
      </div>

      <div className="budget-item-price">
        {/* Price of the item */}
        <input
          type="number"
          value={props.budgetItem.price}
          onChange={(event) => props.handleItemUpdate(event.target.value, props.budgetItem.id, 'price')}
        />
        {' '}
        <span>{props.budgetCurrency}</span>
      </div>

      <div className="budget-item-remove">
        {/* Delete item */}
        <button className="btn btn-remove" onClick={() => props.handleItemRemove(props.budgetItem.id)}><IconBin /></button>
      </div>
    </div>
  )
}

export default BudgetItem

BudgetList component

Next, let’s create the BudgetList component. This component will be very simple and short. Similarly to the BudgetItem, there will also be no logic.

// Import dependencies
import * as React from 'react'

// Import interfaces
import { BudgetItemObjInterface, BudgetListInterface } from './../interfaces'

// Import components
import BudgetItem from './budget-item'

const BudgetList = (props: BudgetListInterface) => {
  return (
    <div className="budget-list">
      {props.budgetItems.map((item: BudgetItemObjInterface) => {
        return (
          <BudgetItem
            key={item.id}
            budgetCurrency={props.budgetCurrency}
            budgetItem={item}
            handleItemUpdate={props.handleItemUpdate}
            handleItemRemove={props.handleItemRemove}
          />
        )
      })}
    </div>
  )
}

export default BudgetList

BudgetItemAdd component

This, the BudgetItemAdd component, will be the last component you will in this, first, part of this tutorial. This component will be a modal dialog that will allow you to add new item on your list, right on the homepage. Unlike the previous component, this component will contain some logic.

About the logic. At the top of this component, you will use React useState hook to create four state, one state for every input. Next, you will create handleFormSubmit function. When triggered, this function will process state for every input, use shortid package to generate unique id, create new item, reset the form and close the modal dialog.

About the structure. There will be fieldset with label and input for date of payment, item title, item price and also checkbox for marking the item as paid. Then, there will be a button, input (type submit). All form elements will be wrapped inside a form. This will be wrapped inside a div a modal-dialog, with a button to close he modal dialog.

As the last thing, this dialog will be wrapped inside another div, a modal-wrapper. This div will work as an overlay when the modal dialog for adding new item will be visible.

About the button. The reason for using input is that you want to trigger submit event on the form. When this happens, you can use the handleFormSubmit function to handle this event and create new item. To do this, attach the handleFormSubmit function as a handler for onSubmit event on the form element.

Note: You don’t have to use form with “submit” input. You can also use a “div” with “button”. In that case, attach the “handleFormSubmit” function on that button as a handler for “onClick” event. This way, everything will work as well.

// Import dependencies
import * as React from 'react'
import shortid from 'shortid'

// Import interface
import { BudgetItemAddInterface } from './../interfaces'

const BudgetItemAdd = (props: BudgetItemAddInterface) => {
  // Prepare BudgetItemAdd states
  const [date, setDate] = React.useState('')
  const [title, setTitle] = React.useState('')
  const [price, setPrice] = React.useState(0)
  const [isPaid, setIsPaid] = React.useState(false)

  function handleFormSubmit(event: React.FormEvent<HTMLFormElement>) {
    // Prevent form from submitting
    event.preventDefault()

    // Create new item
    props.handleAddItem({
      date: date,
      title: title,
      price: price,
      isPaid: isPaid,
      id: shortid.generate()
    })

    // Reset form state
    setDate('')
    setTitle('')
    setPrice(0)
    setIsPaid(false)

    // Close modal window
    props.handleShowAddItem(!props.showAddItem)
  }

  return (
    <div className="modal-wrapper">
      <div className="modal-dialog">
        <button className="btn btn-cross" onClick={() => props.handleShowAddItem(!props.showAddItem)}>⨯</button>

        <form onSubmit={handleFormSubmit}>
          <fieldset>
            {/* Date the item was added */}
            <label htmlFor="date">Date of payment:</label>

            <input
              type="date"
              id="date"
              value={date}
              onChange={(event) => setDate(event.target.value)}
              required={true}
            />
          </fieldset>

          <fieldset>
            {/* Title of the item */}
            <label htmlFor="title">Item name:</label>

            <input
              type="text"
              id="title"
              value={title}
              onChange={(event) => setTitle(event.target.value)}
              required={true}
            />
          </fieldset>

          <fieldset>
            {/* Price of the item */}
            <label htmlFor="price">Item price:</label>

            <input
              type="number"
              id="price"
              value={price}
              onChange={(event) => setPrice(parseInt(event.target.value, 10))}
              min="0"
              step="1"
              required={true}
            />
          </fieldset>

          <fieldset>
            {/* Mark as paid */}
            <input
              className="custom-checkbox-checkbox"
              type="checkbox"
              id="isPaid"
              checked={isPaid}
              onChange={() => setIsPaid(!isPaid)}
            />

            <label className="custom-checkbox-label" htmlFor="isPaid"> Item is already paid</label>
          </fieldset>

          <fieldset>
            <input
              className="btn btn-add"
              type="submit"
              value="+ Add item"
            />
          </fieldset>
        </form>
      </div>
    </div>
  )
}

export default BudgetItemAdd

Conclusion: How to build a budget app with React, TypeScript & Web Storage API

This is it for the first part of this tutorial. Let’s recap. Today, you have set up the project workflow, along with installing additional dependencies, and configuring TypeScript. Next, you’ve prepared interfaces for some components, two icon components for UI and dataset for datalist with currency codes that will be on Settings page.

Lastly, you’ve built the first components for your budget app, the BudgetItem, BudgetList and BudgetItemAdd. In the next part, you will finish this tutorial by creating ItemItemAdd component, Homepage, Settings page, routing for these pages and implement web storage API to store your data.

At the top of this, you will also add some styles to make your budget app look great. But, that, is on the program for the next part. Until then, have a great day and stay tuned.

If you liked this article, please subscribe so you don't miss any future post.

If you'd like to support me and this blog, you can become a patron, or you can buy me a coffee 🙂

By Alex Devero

I'm Founder/CEO of DEVERO Corporation. Entrepreneur, designer, developer. My mission and MTP is to accelerate the development of humankind through technology.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.