ReactT Light_870x220

Types are now one of the most talked about topics in every developer community right now. People are starting to like to type their code and adapt it as a priority in their projects. By typing our code, we get our code safer, more concise, and free from some basic and dumb errors that we might face in development.

After creating an app in React with TypeScript, you'll find TypeScript so enjoyable, you'll not want to start another project without it. We can set up a modern app using React and TypeScript pretty easily - we can use create-react-app and add TypeScript to it. So, let's get started with our expense app.

Getting Started

To start our expense app, we're going to use create-react-app and add a --typescript in the end. So in your terminal, run the following command:

create-react-app expenses-app --typescript

Now that we have our project, we're going to install some dependencies that we're going to need. We'll use formik and yup for our form validation, and dinero.js to format our values. To get yup and dinero.js working with TypeScript, we need to add the TypeScript definitions for each dependency. So in your terminal, run the following command:

yarn add formik yup @types/yup dinero.js @types/dinero.js

Now that we have that all set up, we're going to create the components for our app. So, let's create our folder structure.

Helpers

First, we're going to set up the folders structure of our project. We're going to have the following structure:

expenses-app
|
|- build
|- node_modules
|- public
| |- favicon.ico
| |- index.html
| |- manifest.json
|- src
| |- components
| |- Add
| |- Budget
| |- Expenses
| |- helpers
| |- types
| |- hooks
|- .gitignore
|- package.json
|- tsconfig.json
|- README.md

This is going to be our folder structure. Inside our types folder, we're going to create an index.ts file. And inside that file we'll create an interface for each Expense that we're going to need:

export interface Expense {
  type: string;
  value: number;
  description: string;
}

So now, we have an interface for each Expense that we're going to create later. Each Expense can have the properties, type, value and description.

Inside the hooks folder, create a custom hook called useBudget. Also create a file, useBudget.ts where we'll import the useState hook from React, and our Expense interface:

import { useState } from 'react';
import { Expense } from '../types/index';

Next, let's create a custom hook called useBudget:

const useBudget = () => {
  const [expenses, setExpenses] = useState([]);
  // ...
};

Our expense app is going to be pretty similar to a todo: we're going to have a function to add an expense, and a function to delete an expense. Simple as that. Let's create a function called addExpense() to add an expense:

const addExpense = (expense: Expense) => {
  const newExpenses = [...expenses, { expense }];
  setExpenses(newExpenses);
  console.log(newExpenses);
};

Pretty similar to this function, we're going to create a function to delete an expense, called deleteExpense():

const deleteExpense = (index: number) => {
  const newExpenses = [...expenses];
  newExpenses.splice(index, 1);
  setExpenses(newExpenses);
};

Our final useBudget custom hook should look like this:

const useBudget = () => {
  const [expenses, setExpenses] = useState([]);
  const addExpense = (expense: Expense) => {
    const newExpenses = [...expenses, { expense }];
    setExpenses(newExpenses);
    console.log(newExpenses);
  };
  const deleteExpense = (index: number) => {
    const newExpenses = [...expenses];
    newExpenses.splice(index, 1);
    setExpenses(newExpenses);
  };
  return { expenses, addExpense, deleteExpense };
};

Now that we have our custom hook created, we're going inside the helpers folder and creating two (2) files: formatExpense.ts and totals.ts. formatExpense.ts will contain a function to format values:

import Dinero from 'dinero.js';

const formatExpense = (amount: number) =>
  Dinero({ amount }).setLocale("en-US").toFormat();
export default formatExpense;

Pretty simple. We imported Dinero, and created a function called formatExpense. We passed an amount as an argument to that function, and formatted it to en-US.

Now, let's go to the totals.ts file, and inside that file we're going to have two functions: totalValue() and totalAmount(). The totalValue() function is going to return to us the total value of each value that we have, whether an income or an expense. So now, inside our file, let's import our formatExpense() function that we created earlier and create our totalValue() function:

import formatExpense from './formatExpense';

export const totalValue = (expenses: any, type: string) => {
  const amount = expenses
    .filter(({ expense }: any) => expense.type === type)
    .map(({ expense }) => expense.value)
    .reduce((previousValue, currentValue) => previousValue + currentValue, 0);

  const formattedAmount = formatExpense(amount);
  return formattedAmount;
};

In that function, we have two arguments: the expenses state that we're going to pass, and the type of value — either a + that means income or a - that means expense. After that, we're going to return the total value formatted using the formatExpense function that we created.

Now, let's create our other function called totalAmount, which is going to return to us the total amount that we have. It's going to be pretty similar to the last one, so let's create our function:

export const totalAmount = (expenses: any) => {
  const totalIncomes = expenses
    .filter(({ expense }: any) => expense.type === "+")
    .map(({ expense }) => expense.value)
    .reduce((previousValue, currentValue) => previousValue + currentValue, 0);
  const totalExpenses = expenses
    .filter(({ expense }: any) => expense.type === "-")
    .map(({ expense }) => expense.value)
    .reduce((previousValue, currentValue) => previousValue + currentValue, 0);
  const totalAmount = formatExpense(totalIncomes - totalExpenses);
  return totalAmount;
};

We're getting our total income value and our total expenses value and calculating them to be our total amount.

Now that we have all of our helper functions ready, let's start to create our React components.

Components

Now, we'll go to our main file, index.tsx inside our src folder, and put in the following code:

import React, { Fragment } from 'react';
import ReactDOM from 'react-dom';
import App from './components/App';
import * as serviceWorker from './serviceWorker';

ReactDOM.render(<App />, document.getElementById('root'));

Now that we have our index.tsx all set up, let's go to our components folder and create an App.tsx file for our main React component. Inside that App.tsx file, we're going to import our custom useBudget hook, and pass it as a prop to our other components:

import React from 'react';
import Budget from './Budget/Budget';
import Add from './Add/Add';
import Expenses from './Expenses/Expenses';
import useBudget from '../hooks/useBudget';

const App = () => {
  const { expenses, addExpense, deleteExpense } = useBudget();
  return (
    <div>
      <Budget expenses={expenses} />
      <Add expenses={expenses} addExpense={addExpense} />
      <Expenses expenses={expenses} deleteExpense={deleteExpense} />
    </div>
  );
};

export default App;

We're passing our expenses to the Add and Expenses component, and also our functions to add and delete an expense. So now, let's go to our Budget folder, and inside that folder create a file called Budget.tsx and put the following code:

import React from 'react';
import Value from './Value/Value';
import Available from './Available/Available';
import Amount from './Amount/Amount';
import { Expense } from '../../types/index';
import { totalValue, totalAmount } from '../../helpers/totals';

interface BudgetProps {
  expenses: Expense[];
}

const Budget: React.FC<BudgetProps> = ({ expenses }) => (
  <div>
    <header>
      <Available month="June 2019" />
      <Value value={totalAmount(expenses)} />
    </header>
    <Amount type={"Income"}
            amount={totalValue(expenses, "+")}
            backgroundColor="#0EAD69" />
    <Amount type={"Expenses"}
            amount={totalValue(expenses, "-")}
            backgroundColor="#DD1C1A" />
  </div>
);

export default Budget;

Inside our Budget component, we have three components: Available which is going to display the actual month, Value which is going to be the component that displays the total amount that we have, and an Amount component which is going to render a specific value for each type that we have, income or expense.

So now, inside the Budget folder, let's create a folder for each component: Available, Value, and Amount. Inside the Value folder, create a file called Value.tsx, and put the following code:

import React from 'react';

interface ValueProps {
  value: number;
}

const Value: React.FC<ValueProps> = ({ value }) => (
  <h1>{value}</h1>
);

export default Value;

Now, inside our Available folder, let's create a file called Available.tsx and put the following code:

import React from 'react';

interface AvailableProps {
  month: string;
}

const Available: React.FC<AvailableProps> = ({ month }) => (
  <h1>Available budget in {month}:</h1>
);

export default Available;

Next, inside our Amount folder let's create a file called Amount.tsx and put the following code:

import React from 'react';
import AmountValue from './AmountValue/AmountValue';
import AmountType from './AmountType/AmountType';

interface AmountProps {
  amount: number;
  type: string;
}

const Amount: React.FC<AmountProps> = ({ amount, type }) => (
  <div>
    <AmountType amountType={type} />
    <AmountValue amountValue={amount} />
  </div>
);

export default Amount;

And inside our folder, we're going to create a folder called AmountValue, and a file inside that folder called AmountValue.tsx. Inside that file, we're going to put the following code:

import React from 'react';

interface AmountValueProps {
  amountValue: number;
}

const AmountValue: React.FC<AmountValueProps> = ({ amountValue }) => (
  <h1>{amountValue}</h1>
);

export default AmountValue;

Now, we're still in the Amount folder, and we're going to create the last folder: AmountType. Let's also create a file called AmountType.tsx with following code:

import React from 'react';

interface AmountTypeProps {
  amountType: string;
}

const AmountType: React.FC<AmountTypeProps> = ({ amountType }) => (
  <h1>{amountType}</h1>
);

export default AmountType;

With the Budget folder ready, create a file called Add.tsx in the Add folder. Inside that file, we're going to use formik and yup to validate our form, so let's import some things and create some interfaces to use in our form:

import React from 'react';
import * as Yup from 'yup';
import { withFormik, FormikProps } from 'formik';

interface FormValues {
  type: string;
  value: number;
  description: string;
}

interface OtherProps {
  expenses: any;
  addExpense: (expense: Expense) => any;
}

interface MyFormProps {
  expenses: any;
  addExpense: (expense: Expense) => any;
}

Then we're going to create a component called InnerForm:

const InnerForm = (props: OtherProps & FormikProps<FormValues>) => {
  const {
    values,
    errors,
    touched,
    handleChange,
    handleBlur,
    handleSubmit,
    isSubmitting
  } = props;

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <select name="type"
                value={values.type}
                onChange={handleChange}
                onBlur={handleBlur}>
          <option value="" label="Select">Select</option>
          <option value="+" label="+">+</option>
          <option value="-" label="-">-</option>
        </select>
        <input width={100}
               maxWidth={120}
               placeholder="Value"
               type="number"
               name="value"
               onChange={handleChange}
               onBlur={handleBlur}
               value={values.value} />
        <input width={100}
               maxWidth={300}
               placeholder="Description"
               type="text"
               name="description"
               onChange={handleChange}
               onBlur={handleBlur}
               value={values.description} />
        <button width={100}
                type="submit"
                disabled={isSubmitting ||
                          !!(errors.type && touched.type) ||
                          !!(errors.value && touched.value) ||
                          !!(errors.description && touched.description)}>
          Add
        </button>
      </form>
    </div>
  );
};

In the same file, let's add the form validation using yup:

const Add = withFormik<MyFormProps, FormValues>({
  mapPropsToValues: () => ({
    type: "",
    value: 0,
    description: ""
  }),
  validationSchema: Yup.object().shape({
    type: Yup.string().required("Nome ' obrigat'rio"),
    value: Yup.number().required("Value obrigat'ria"),
    description: Yup.string().required("Description obrigat'ria")
  }),
  handleSubmit(
    { type, value, description }: FormValues,
    { props, setSubmitting }
  ) {
    setTimeout(() => {
      props.addExpense({ type, value, description });
      setSubmitting(false);
    }, 1000);
  }
})(InnerForm);

export default Add;

All right, we have our form all ready to go. So now we're going to create the last part of our app. Let's go to our Expenses folder, and inside that folder create a file called Expense.tsx and another two folders: Income and Expense. Inside our Expense.tsx file, let's put the following code:

import React from 'react';
import Income from './Income/Income';
import Expense from './Expense/Expense';

interface ExpensesProps {
  expenses: any;
  deleteExpense: (index: number) => any;
}

const Expenses: React.FC<ExpensesProps> = ({ expenses, deleteExpense }) => (
  <div>
    <Income expenses={expenses} deleteExpense={deleteExpense} />
    <Expense expenses={expenses} deleteExpense={deleteExpense} />
  </div>
);

export default Expenses;

In the Income folder, we're going to create a file called Income.tsx and a folder called IncomeItem. In Income.tsx, let's put the following code:

import React from 'react';
import IncomeItem from './IncomeItem/IncomeItem';

interface IncomeProps {
  expenses: any;
  deleteExpense: any;
}

const Income: React.FC<IncomeProps> = ({
  expenses,
  deleteExpense
}) => {
  const incomes = expenses.filter(({ expense }: any) => expense.type === "+");
  return (
    <div>
      <h1>Income</h1>
      <div>
        {incomes.map(({ expense }: any, index: number) => (
          <IncomeItem index={index}
                      key={index}
                      type={expense.type}
                      value={expense.value}
                      description={expense.description}
                      deleteExpense={deleteExpense} />
        ))}
      </div>
    </div>
  );
};

export default Income;

Now, inside the IncomeItem folder, let's create an IncomeItem.tsx file and put the following code:

import React from 'react';
import formatExpense from '../../../../helpers/formatExpense';

interface IncomeItemProps {
  index: number;
  type: string;
  value: number;
  description: string;
  deleteExpense: (index: number) => any;
}

const IncomeItem: React.FC<IncomeItemProps> = ({
  index,
  type,
  value,
  description,
  deleteExpense
}) => (
  <div onClick={() => deleteExpense(index)}>
    <h1>{description}</h1>
    <h3>{formatExpense(value)}</h3>
  </div>
);

export default IncomeItem;

Now, let's go to our Expense folder and create an Expense.tsx file:

import React from 'react';
import ExpenseItem from './ExpenseItem/ExpenseItem';

interface ExpenseProps {
  expenses: string;
  deleteExpense: (index: number) => any;
}

const Expense: React.FC<ExpenseProps> = ({
  expenses,
  deleteExpense
}) => {
  const newExpenses = expenses.filter(({ expense }: any) => expense.type === "-");
  return (
    <div>
      <h1>Expense</h1>
      <div>
        {newExpenses.map(({ expense }: any, index: number) => (
          <ExpenseItem index={index}
                       key={index}
                       type={expense.type}
                       value={expense.value}
                       description={expense.description}
                       deleteExpense={deleteExpense} />
        ))}
      </div>
    </div>
  );
};

export default Expense;

Finally, the last component of our app! Let's create a folder called ExpenseItem and inside that folder create a file called ExpenseItem.tsx and put the following code:

import React from 'react';
import formatExpense from '../../../../helpers/formatExpense';

interface ExpenseItemProps {
  index: number;
  type: string;
  value: number;
  description: string;
  deleteExpense: (index: number) => any;
}

const ExpenseItem: React.FC<ExpenseItemProps> = ({
  index,
  type,
  value,
  description,
  deleteExpense
}) => (
  <div onClick={() => deleteExpense(index)}>
    <h1>{description}</h1>
    <h3>{formatExpense(value)}</h3>
  </div>
);

export default ExpenseItem;

Building Apps with KendoReact

Now that we have our application working fine, if you want to build a great and beautiful interface for it, you can check out KendoReact. KendoReact is a complete UI component library for React, built with high-quality and responsive components.

It includes all the components that you need for everything from a simple basic app to complex apps, so with KendoReact you can focus on what matters in your app and stop trying to build complex UI components.

Conclusion

In this article, we created an expense app from the beginning with React and TypeScript, learned how we can integrate interfaces with React Components, and also used React Hooks to deal with our state management. This is a simple app just to show how powerful React with TypeScript can be.


Leonardo Maldonado
About the Author

Leonardo Maldonado

Leonardo is a Full Stack Developer, working with everything React-related, and loves to write about React and GraphQL to help developers. He also created the 33 JavaScript Concepts.

Related Posts

Comments

Comments are disabled in preview mode.