How to create a simple Expense Tracker App using React Context with useReducer and Typescript.

How to create a simple Expense Tracker App using React Context with useReducer and Typescript.

Typescript is hard , they said . Making this simple application would give us an idea of how simple it is to use typescript in our projects .

What is typescript?

Typescript is a typed superset of Javascript that complies to plain Javascript . It allows both you and I to specify types for values in your code at compile time , so that you can develop applications with more confidence .

Organizing our Project

First , we create a new react project by typing npx create-react-app Expense-app typescript into our integrated terminal .

npx create-react-app Expense-app typescript

We would organize our files in this order .

  • GlobalState.tsx : Component to store all data using useReducer and context API

  • AddTransaction.tsx : Component for adding the transactions that we input .

  • Balances.tsx : Component to store the total balance we would get after each transaction .

  • Header.tsx : Component for just the title on top .

  • IncomeExpenses.tsx : Component for calculating the total income and total expenses .

  • TransactionList.tsx : Component that serves as container for the transactionss.tsx .We would pass the data here to the transactionss component as props . Screenshot (125)h.png

Building Our Project

We start by creating a folder called Context , that would contain our GlobalState.tsx file .

cd Expense-Tracker/src
mkdir Context
cd Context
touch GlobalState.tsx

We can initialize a context api with any value and the useReducer , Dispatch which we would be using later on . Here , i am using an empty object .

import React, { createContext , useReducer , Dispatch } from "react"
export const UserContext = createContext({ })

Let's add an initial state for our createContext , but first , we create types for it for type checking which contains an array of objects symbolized by " []" . Every transactions which is an object would have a property of Id which would be a string , amount which would be a number and text which would be a string .

type First = {
  transactions: {
    id: number;
    text: string;
    amount: number;
  }[];
}

We create the initialState with hardcoded figures in it . We add the type First to ensure that what would be passed in would only contain what is defined in type First .

const initialState: First = {
  transactions: [
    { id: 1, text: "Flower", amount: -20 },
    { id: 2, text: "Salary", amount: 300 },
    { id: 3, text: "Book", amount: -10 },
    { id: 4, text: "Camera", amount: 150 },
  ],
};

We modify our previous createContext to contain the initialState and type-check our values by adding the type First to it .

export const UserContext = React.createContext<{state: First}>({ state: initialState});

Let's make reducers and actions to create and delete transactions .We start by making the types for the action.Read more about it here . The type ADD_TRANSACTION would have a payload of id as a number or any , text as a string and amount as a number .The type DELETE_TRANSACTION would have a payload of id as string , since we want to delete the transaction based on what we click .

type Actions =
  | {
      type: "ADD_TRANSACTION";
      payload: { id: number | any; amount: number; text: string };
    }
  | {
      type: "DELETE_TRANSACTION";
      payload: number;
    };

Next , we create our reducer which receives two arguments , state and action . First is the state, that we are passing when using useReducer hook and the action is the object that represents events such as a click that would change the state .

  • For the ADD_TRANSACTION , we attach the payload (our input) to the state
  • For the DELETE_TRANSACTION ,we remove the id from the state , resulting to the deletion of the associated transaction.

we type-check using the type Actions and First

const reducer = (state: First, action: Actions) => {
  switch (action.type) {
    case "ADD_TRANSACTION":
      return {
        ...state,
        transactions: [
          ...state.transactions,
          {
            id: action.payload.id,
            text: action.payload.text,
            amount: action.payload.amount,
          },
        ],
      };
    case "DELETE_TRANSACTION":
      return {
        ...state,
        transactions: state.transactions.filter(
          (transaction) => transaction.id !== action.payload
        ),
      };

    default:
      return state;
  }
};

We re-modify our previous createContext to contain the Actions and type-check our values by adding the type Actions to it . So it would look like this :

export const UserContext =createContext<{
  state: First;
  dispatch: Dispatch<Actions>;
}>({ state: initialState, dispatch: () => null });

The next thing we would do is to call the useReducer inside our functional component and pass the reducer and initialState variables into it , which returns the state and the dispatch function .We also create a provider to wrap around all other components by passing the children props into it .

export const GlobalState: React.FC<{}> = ({ children }) => {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <UserContext.Provider value={{ state, dispatch }}>
      {children}
    </UserContext.Provider>
  );
};

So our GlobalState.tsx would finally look like this :

import React, { useReducer, Dispatch } from "react";

type First = {
    transactions: {
        id: number;
        text: string;
        amount: number;
    }[];
};

type Actions = {
    type: "ADD_TRANSACTION";
    payload: {
        id: number | any;
        amount: number;
        text: string;
    };
} | {
    type: "DELETE_TRANSACTION";
    payload: number;
};
const initialState: First = {
  transactions: [
    { id: 1, text: "Flower", amount: -20 },
    { id: 2, text: "Salary", amount: 300 },
    { id: 3, text: "Book", amount: -10 },
    { id: 4, text: "Camera", amount: 150 },
  ],
};

export const UserContext = React.createContext<{
  state: First;
  dispatch: Dispatch<Actions>;
}>({ state: initialState, dispatch: () => null });
const reducer = (state: First, action: Actions) => {
  switch (action.type) {
    case "ADD_TRANSACTION":
      return {
        ...state,
        transactions: [
          ...state.transactions,
          {
            id: action.payload.id,
            text: action.payload.text,
            amount: action.payload.amount,
          },
        ],
      };
    case "DELETE_TRANSACTION":
      return {
        ...state,
        transactions: state.transactions.filter(
          (transaction) => transaction.id !== action.payload
        ),
      };

    default:
      return state;
  }
};


export const GlobalState: React.FC<{}> = ({ children }) => {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <UserContext.Provider value={{ state, dispatch }}>
      {children}
    </UserContext.Provider>
  );
};

We import the GlobalState to the App.tsx and wrap around the components as shown below .

Screenshot (126).png

Styling

Let's add styles in the App.css

:root {
  --box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
}

* {
  box-sizing: border-box;
}

body {
  background-color: #f7f7f7;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  margin: 0;
  font-family: 'Lato', sans-serif;
}

.containers {
  margin: 30px auto;
  width: 350px;
}

h1 {
  letter-spacing: 1px;
  margin: 0;
}

h3 {
  border-bottom: 1px solid #bbb;
  padding-bottom: 10px;
  margin: 40px 0 10px;
}

h4 {
  margin: 0;
  text-transform: uppercase;
}

.inc-exp-container {
  background-color: #fff;
  box-shadow: var(--box-shadow);
  padding: 20px;
  display: flex;
  justify-content: space-between;
  margin: 20px 0;
}

.inc-exp-container>div {
  flex: 1;
  text-align: center;
}

.inc-exp-container>div:first-of-type {
  border-right: 1px solid #dedede;
}

.money {
  font-size: 20px;
  letter-spacing: 1px;
  margin: 5px 0;
}

.money.plus {
  color: #2ecc71;
}

.money.minus {
  color: #c0392b;
}

label {
  display: inline-block;
  margin: 10px 0;
}

input[type='text'],
input[type='number'] {
  border: 1px solid #dedede;
  border-radius: 2px;
  display: block;
  font-size: 16px;
  padding: 10px;
  width: 100%;
}

.btn {
  cursor: pointer;
  background-color: #9c88ff;
  box-shadow: var(--box-shadow);
  color: #fff;
  border: 0;
  display: block;
  font-size: 16px;
  margin: 10px 0 30px;
  padding: 10px;
  width: 100%;
}

.btn:focus,
.delete-btn:focus {
  outline: 0;
}

.list {
  list-style-type: none;
  padding: 0;
  margin-bottom: 40px;
}

.list li {
  background-color: #fff;
  box-shadow: var(--box-shadow);
  color: #333;
  display: flex;
  justify-content: space-between;
  position: relative;
  padding: 10px;
  margin: 10px 0;
}

.list li.plus {
  border-right: 5px solid #2ecc71;
}

.list li.minus {
  border-right: 5px solid #c0392b;
}

.delete-btn {
  cursor: pointer;
  background-color: #e74c3c;
  border: 0;
  color: #fff;
  font-size: 20px;
  line-height: 20px;
  padding: 2px 5px;
  position: absolute;
  top: 50%;
  left: 0;
  transform: translate(-100%, -50%);
  opacity: 0;
  transition: opacity 0.3s ease;
}

.list li:hover .delete-btn {
  opacity: 1;
}

State Management

For the AddTransaction Component , We import UseContext and UserContext from react and GlobalState.tsx respectively .We also destructure the dispatch from the UserContext . This component would dispatch the action ADD_TRANSACTION. We input our values and generate a random number for the id .The input fields are reset after each submission . The useState for storing the amount value is set to an initial value of 5 and set to a number. The useState for storing the text value is set to an initial value of "" with and set to a string .

import React, { useContext } from "react";
import { UserContext } from "../Components/Context/GlobalState";
export const AddTransaction: React.FC<{}> = () => {
  const { dispatch } = useContext(UserContext);

  const [text, SetText] = React.useState<string>("");
  const [amount, Setamount] = React.useState<number>(5);

  const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();

    dispatch({
      type: "ADD_TRANSACTION",
      payload: {
        id: Math.floor(Math.random() * 10000),
        text,
        amount,
      },
    });
    SetText("type again");

    Setamount(0);
  };

  return (
    <>
      <h3>Add new transaction</h3>
      <form onSubmit={onSubmit}>
        <div className="form-controls">
          <label htmlFor="text">Text</label>
          <input
            type="text"
            value={text}
            onChange={(e) => {
              SetText(e.target.value);
            }}
            placeholder="Enter text..."
          />
        </div>
        <div className="form-controls">
          <label htmlFor="amount">
            Amount <br />
            (negative - expense, positive - income)
          </label>
          <input
            type="number"
            value={amount}
            onChange={(e) => {
              Setamount(parseInt(e.target.value));
            }}
            placeholder="Enter amount..."
          />
        </div>
        <button className="btn">Add transaction</button>
      </form>
    </>
  );
};

For the TransactionList component , We import the userContext from the GlobalState and import useContext from react to consume the data being passed and destructure to get the transactions .We also import Transactionss.tsx and pass the data(...transaction) as props into it .

import React, { useContext } from "react";
import { UserContext } from "../Components/Context/GlobalState";
import { Transactionss } from "./Transactionss";

export const TransactionList: React.FC<{}> = () => {
  const { state } = useContext(UserContext);
  const { transactions } = state;
  return (
    <>
      <h3>History</h3>
      <ul id="list" className="list">
        {transactions.map((transaction) => (
          <Transactionss key={transaction.id} {...transaction} />
        ))}
      </ul>
    </>
  );
};

For the Transactionss component , just like the AddTransaction component , we pass the import both useContext and UserContext .We destructure the dispatch from the UserContext and also pass the props from the TransactionList.tsx . The values from the props are type-checked by adding a type Second .This component as said earlier , dispatches the DELETE_TRANSACTION action

import React, { useContext } from "react";
import { UserContext } from "../Components/Context/GlobalState";
type Second = {
  text: string;
  amount: number;
  id: number;
}

export const Transactionss: React.FC<Second> = (props) => {
  const sign = props.amount < 0 ? "-" : "+";
  const { dispatch } = useContext(UserContext);

  return (
    <li className={props.amount < 0 ? "minus" : "plus"}>
      {props.text}{" "}
      <span>
        {sign}
        {Math.abs(props.amount)} Naira
      </span>
      <button
        onClick={() => {
          dispatch({ type: "DELETE_TRANSACTION", payload: props.id });
        }}
        className="delete-btn"
      >
        x
      </button>
    </li>
  );
};

For the Balances component , the only new thing is the reduce function , which we use to add all transactions together to get the total balance .

import React, { useContext } from "react";
import { UserContext } from "../Components/Context/GlobalState";
export const Balances: React.FC<{}> = () => {
  const { state } = useContext(UserContext);
  const { transactions } = state;
  const amounts = transactions.map((transaction) => transaction.amount);
  const initial = 0;
  const total = amounts.reduce(
    (previous, current) => (previous += current),
    initial
  );
  return (
    <div>
      <h4>Your Balance</h4>
      <h1 id="balance">{total} Naira</h1>
    </div>
  );
};

The Header component just renders the text Expense Tracker

import React from "react";
export const Header: React.FC<{}> = () => {
  return <h2>Expense Tracker</h2>;
};

The last component to deal with is the IncomeExpenses component . This component displays the income and expenses.We get the transactions , filter them and add them together .

import React, { useContext } from "react";
import { UserContext } from "../Components/Context/GlobalState";

export const IncomeExpenses: React.FC<{}> = () => {
  const { state } = useContext(UserContext);
  const { transactions } = state;
  const amount = transactions.map((transaction) => transaction.amount);
  const initial = 0;
  const income = amount
    .filter((items) => items > 0)
    .reduce((previous, current) => (previous += current), initial)
    .toFixed(2);

  const expense = (
    amount
      .filter((items) => items < 0)
      .reduce((previous, current) => (previous += current), initial) * 1
  ).toFixed(2);
  return (
    <div className="inc-exp-container">
      <div>
        <h4>Income</h4>
        <p id="money-plus" className="money plus">
          {income}
        </p>
      </div>
      <div>
        <h4>Expense</h4>
        <p id="money-minus" className="money minus">
          {expense}
        </p>
      </div>
    </div>
  );
};

Run npm start on the root folder and see our application come to life . If you encountered any error , you can check out the project on my github page here

Screenshot (127).png

Congratulations

Our application is now running on our local machine .Thank you for coming this far. I hope it has been helpful . In the meantime, please ask questions or concerns in the comments . See ya!