Go Back to Home
image featured post
React JS

Building a Finance Web Application with React, TypeScript, Styled-Components, and REST API (Part 2)

  • Rui Vergani Neto
  • 25, June, 2024
  • 20 min read
Search through topics

“Passion is the key to extraordinary work. If you haven’t discovered your passion yet, keep searching. Don’t compromise. When you find it, you’ll know deep within your heart.”

Steve Jobs

Project Description

The project DT Money is a financial management application designed to help users track their expenses, manage budgets, and gain insights into their financial health. Leveraging the power of ReactJS and TypeScript, DT Money provides a seamless, efficient, and high-performance user experience.

REST API Integration

In this phase of the project, we will be integrating a connection with a REST API to enhance the application’s functionality.

Configure JSON Server

We will utilize JSON Server to create a mock REST API, providing a quick and effective back-end solution for prototyping and development. The JSON Server offers essential features such as routing, filtering, pagination, sorting, and a database, all of which are necessary for our project. By using the provided GitHub repository, we can easily access these functionalities to support our development needs.

Install JSON Server

npm install -g json-server@0.17.4    # NPM
yarn global add json-server@0.17.4   # Yarn
npm add -g json-server@0.17.4       # PNPM

Create a server.json file with some data

{
  "transactions": [
    {
      "id": 1,
      "description": "Development of websites",
      "type": "income",
      "category": "Sell",
      "price": 14000,
      "createdAt": "2024-06-09T16:19:54.812Z"
    },
    {
      "id": 2,
      "description": "Burguer",
      "type": "outcome",
      "category": "Food",
      "price": 50,
      "createdAt": "2024-06-09T16:19:54.812Z"
    }
  ]
}

Initialize the server

You can also add delay to this server, and change the port number if you get any errors.

npx json-server server.json

Start HTTP Request using the API

Before we begin, we’ll be using JSON Server to simulate the back-end of our application. In this step, our task is to load the list of transactions from the API. There are a few points we will need to consider to understand the code below:

  • Avoid fetching data from the API all the time the components enter a cycle of render, to fix that we will be using useEffect hook from React.
  • Understand why we use async/await keywords in JavaScript.

Understanding async/await

Imagine you are baking a cake. Some steps need you to wait, like baking the cake in the oven. Let’s see how this process would look with and without async/await.

Without async/await (using .then):

Fetch Ingredients:

  • Go to the store (this takes some time).
  • Once you get the ingredients, start mixing them (this happens after the store trip is done).
goToStore()
  .then((ingredients) => {
    return mixIngredients(ingredients);
  })
  .then((batter) => {
    return bakeCake(batter);
  })
  .then((cake) => {
    console.log('Cake is ready!', cake);
  });

With async/await:

Fetch Ingredients:

  • Go to the store and wait until you have the ingredients.
  • Mix the ingredients once you have them.
  • Bake the cake once the batter is ready.
async function makeCake() {
  const ingredients = await goToStore(); // Wait for the store trip to finish
  const batter = await mixIngredients(ingredients); // Wait for mixing to finish
  const cake = await bakeCake(batter); // Wait for the cake to bake

  console.log('Cake is ready!', cake);
}

makeCake();

Breaking it Down:

  • async function makeCake(): This function will handle our cake-making process and return a Promise.
  • await goToStore(): We wait for the store trip to complete before moving on.
  • await mixIngredients(ingredients): We wait for the mixing to finish before moving on.
  • await bakeCake(batter): We wait for the baking to finish before celebrating our cake.

It’s important to understand the async/await concept to relate to our API loadTransactions() function. In our scenario by default useEffect is not an async function so we need to create an async function inside the useEffect and use that function.

Because we are using Typescript we need to declare the interface and @type of our useState hook, in this case the useState hook is the only way for us to store information in a variable and be able to manage that data:

interface Transactions {
  id: number,
  description: string;
  type: 'income' | 'outcome';
  price: number;
  category: string;
  createdAt: string;
}
const [transactions, setTransactions] = useState<Transactions[]>([])

Here’s the code for Transactions/index.tsx file.

import { useEffect, useState } from "react";
import Header from "../../components/Header";
import { Summary } from "../../components/Summary";
import { SearchForm } from "./components/SearchForm";
import { PriceHighlight, TransactionsContainer, TransactionsTable } from "./styles";

interface Transactions {
  id: number,
  description: string;
  type: 'income' | 'outcome';
  price: number;
  category: string;
  createdAt: string;
}
export function Transactions() {
  const [transactions, setTransactions] = useState<Transactions[]>([])

  useEffect(() => { // useEffect can not be async
    async function loadTransactions() {
      const response = await fetch('http://localhost:3000/transactions')
      const data = await response.json();

      setTransactions(data)
    }
    loadTransactions()
  }, [])

  return (
    <div>
      <Header />
      <Summary />
      {/* Header Table */}
      <TransactionsContainer>
        <SearchForm/>
        <TransactionsTable>
          <tbody>
            {
              transactions.map(transaction => {
                return (
                  <tr key={transaction.id}>
                    <td width="50%">{transaction.description}</td>
                    <td>
                      <PriceHighlight variant={transaction.type}>
                        R$ {transaction.price}
                      </PriceHighlight>
                    </td>
                    <td>{transaction.category}</td>
                    <td>{transaction.createdAt}</td>
                  </tr>
                )
              })
            }
          </tbody>
        </TransactionsTable>
      </TransactionsContainer>
    </div>
  )
}

Understanding Context in React

In React, the “context” concept is a feature that allows you to share data across different components without having to pass props down manually at every level of the component tree.

  • Prop Drilling Problem: In a React application, components often need to share data. The usual way to do this is by passing data (via props) from parent components to child components. However, when many components need the same piece of data, it can become cumbersome and error-prone to pass props through many layers of the component tree. This is known as “prop drilling”.
  • What Context Solves: Context provides a way to pass data through the component tree without having to pass props down manually at every level. It allows you to create a “context” that any component can subscribe to, regardless of its position in the component tree.

Context in a human way to understand

Imagine you have a family living in a house, and every family member needs to know what’s for dinner. If you had to tell each person individually, it would be a lot of work, especially if you have a big family.

Instead, you could put a note on the fridge with the dinner menu. Now, anyone who needs to know what’s for dinner can just look at the fridge. This way, you don’t have to run around the house telling everyone.

In the same way, in a React application, “context” is like that note on the fridge. It’s a central place where you can put information that many parts of your application need.

Create Transactions Context

In our scenario, we will be creating a context for the transaction fetch API, because we will be using this data in many areas of our application, such as Header, Summary, and Transactions index.

1. Create a new directory called contexts inside /src.

2. Create a new file called TransactionsContext.tsx

3. Move part of the code from Transactions/index.tsx into the new Context file.

Here’s the code for this file:

import { createContext, ReactNode, useEffect, useState } from "react";

// @types
interface Transaction {
  id: number,
  description: string;
  type: 'income' | 'outcome';
  price: number;
  category: string;
  createdAt: string;
}
interface TransactionContextType {
  transactions: Transaction[]; // list of transactions
}
interface TransactionsProviderProps {
  children: ReactNode; // any element valid in React
}

// Creating the Context
export const TransactionsContext = createContext({} as TransactionContextType)

// Export the Context.Provider
export function TransactionsProvider({ children }: TransactionsProviderProps) {

  const [transactions, setTransactions] = useState<Transaction[]>([])

  useEffect(() => { // useEffect can not be async
    async function loadTransactions() {
      const response = await fetch('http://localhost:3000/transactions')
      const data = await response.json();
      setTransactions(data);
    }
    loadTransactions();
  }, [])

  // Return Code
  return (
    <TransactionsContext.Provider value={{ transactions }}>
      {children}
    </TransactionsContext.Provider>
  )
}

Note: in the file above we can use the use of interface a lot, this is due to Typescript’s mandatory declaration of types.

Now let’s modify our Transactions/index.tsx file, by adding the useContext hook:

const { transactions } = useContext(TransactionsContext);
import { useContext } from "react";
import Header from "../../components/Header";
import { Summary } from "../../components/Summary";
import { SearchForm } from "./components/SearchForm";
import { PriceHighlight, TransactionsContainer, TransactionsTable } from "./styles";
import { TransactionsContext } from "../../contexts/TransactionsContext";

export function Transactions() {
  const { transactions } = useContext(TransactionsContext);

  return (
    <div>
      <Header />
      <Summary />
      {/* Header Table */}
      <TransactionsContainer>
        <SearchForm/>
        <TransactionsTable>
          <tbody>
            {
              transactions.map(transaction => {
                return (
                  <tr key={transaction.id}>
                    <td width="50%">{transaction.description}</td>
                    <td>
                      <PriceHighlight variant={transaction.type}>
                        R$ {transaction.price}
                      </PriceHighlight>
                    </td>
                    <td>{transaction.category}</td>
                    <td>{transaction.createdAt}</td>
                  </tr>
                )
              })
            }
          </tbody>
        </TransactionsTable>
      </TransactionsContainer>
    </div>
  )
}

Another important modification we need to do is to add the <TransactionsProvider> created previously inside the App.tsx of our application.

import { ThemeProvider } from "styled-components";
import { defaultTheme } from "./styles/themes/default";
import { GlobalStyle } from "./styles/global";
import { Transactions } from "./pages/Transactions";
import { TransactionsProvider } from "./contexts/TransactionsContext";

export function App() {
  return (
    <ThemeProvider theme={defaultTheme}>
      <GlobalStyle />
      <TransactionsProvider>
        <Transactions/>
      </TransactionsProvider>
    </ThemeProvider>
  )
}

Understanding Reduce in React

In React, the reduce method is used to transform an array into a single value or a new data structure. This is achieved by applying a function to each element of the array and accumulating the results.

Initial Data Structure

You start with an array of transactions where each transaction is an object. Each object has at least the following properties: type and price. The type can be either ‘income’ or ‘outcome’, and price is the numerical value of the transaction.

Objective

You want to reduce this array of transactions into a single summary object that tracks the total income, total outcome, and the net total.

We use a method called reduce to process this list of transactions and create a summary. The reduce method works by taking a function that updates an “accumulator” (a running total) as it goes through each item in the list.

Step-by-Step Process

1. Start with an empty summary: We begin with a summary that says we have earned R$0, spent R$0, and our total balance is R$0

{ income: 0, outcome: 0, total: 0 }

2. Go through each transaction one by one:

If the transaction is income:

  • Add the amount to the total earnings.
  • Add the amount to the overall balance.

If the transaction is outcome:

  • Add the amount to the total spending.
  • Subtract the amount from the overall balance.

Explanation for kids of what is Reduce

In React, you might use reduce to manage state or process data. For example, if you have a list of orders and you want to calculate the total amount spent, you could use reduce to sum up the amounts.

const orders = [
  { id: 1, amount: 250 },
  { id: 2, amount: 150 },
  { id: 3, amount: 200 }
];

const totalAmount = orders.reduce((total, order) => total + order.amount, 0);

Here, total starts at 0, and for each order, the amount is added to total. By the end, totalAmount will be the sum of all order amounts.

Another explanation of what is Reduce method

Imagine you have an array of students, and each student has a name and a grade. You want to group these students by their grades into an object where the keys are the grades and the values are arrays of student names.

const students = [
  { name: 'Alice', grade: 'A' },
  { name: 'Bob', grade: 'B' },
  { name: 'Charlie', grade: 'A' },
  { name: 'David', grade: 'C' },
  { name: 'Eve', grade: 'B' },
];

Using the reduce method, we can transform this array into an object that groups students by their grades:

const groupedByGrade = students.reduce((acc, student) => {
  const { grade, name } = student;

  // If the grade doesn't exist in the accumulator, create an empty array for it
  if (!acc[grade]) {
    acc[grade] = [];
  }

  // Add the student's name to the array for their grade
  acc[grade].push(name);

  return acc;
}, {});

Calculating the Summary of Transactions

To calculate the summary of transactions you need to use the reduce method and calculate income, outcome, and total value. Type the following code into the Summary/index.tsx file.

Here’s the code for the index.tsx:

// {income: 0, outcome: 0, total: 0} => reduzir o array (transactions) a uma nova estrutura de dados
  const summary = transactions.reduce(
    (accumulator, transaction) => {
      // Check if the transaction is of type 'income'
      if (transaction.type === 'income') {
        // Add the transaction price to the income and total
        accumulator.income += transaction.price;
        accumulator.total += transaction.price;
      } else {
        // Otherwise, add the transaction price to the outcome and subtract it from the total
        accumulator.outcome += transaction.price;
        accumulator.total -= transaction.price;
      }
      // Return the accumulator for the next iteration
      return accumulator;
    },
    {
      // Initial value of the accumulator
      income: 0,
      outcome: 0,
      total: 0
    }
  );

Format Values – BRL

To format all values of the application we will be creating a new folder called utils containing a file called formatter.ts inside.

File formatter.ts:

// API Intly: JavaScript native API
export const dateFormatter = new Intl.DateTimeFormat('pt-BR');

export const priceFormatter = new Intl.NumberFormat('pt-BR', {
  style: 'currency',
  currency: 'BRL',
});

Now we need to edit all places that contains any dollar or R$ sign value. Go to the index.ts in the Transactions and change the code below:

From this:

<PriceHighlight variant={transaction.type}>
  R$ {transaction.price}
</PriceHighlight>

To this:

Note: we have also validated using the IF statement to check if is the ‘outcome’ variable so we can add a – (minus) sign, and we have added the format for the Date.

 <PriceHighlight variant={transaction.type}>
    {transaction.type === 'outcome' && ' -'}                  
    {priceFormatter.format(transaction.price)}
 </PriceHighlight>
 
 // Code below formats the date
 <td>{dateFormatter.format(new Date (transaction.createdAt))}</td>

Creating our own React Hook

Creating your own custom hooks in React allows you to encapsulate and reuse stateful logic across different components. Custom hooks start with the word “use” and can call other hooks to utilize React’s state and lifecycle features. Here’s a step-by-step explanation of how to create a custom hook in React:

Step-by-Step Guide

1. Create a Function Starting with use:

  • Custom hooks should start with the prefix use to ensure they follow the same naming convention as built-in hooks, which is important for React to be able to enforce the rules of hooks.

2. Implement the Hook Using Built-in Hooks:

  • Inside your custom hook, use built-in hooks such as useState, useEffect, useContext, etc., to implement the desired functionality.

3. Return Necessary Values:

  • Return the state variables, functions, or any other values that the consuming components will need.

4. Use the Custom Hook in Components:

  • Use your custom hook in any functional component where you need the encapsulated logic.

In our application, we have also created our own hook. Create a new folder called hooks and a file called useSummary.tsx and move code from our index.tsx (Summary) into this new React hook:

import { useContext } from "react";
import { TransactionsContext } from "../contexts/TransactionsContext";

export function useSummary() {
  const { transactions } = useContext(TransactionsContext);

  // {income: 0, outcome: 0, total: 0} => reduzir o array (transactions) a uma nova estrutura de dados
  const summary = transactions.reduce(
    (accumulator, transaction) => {
      // Check if the transaction is of type 'income'
      if (transaction.type === 'income') {
        // Add the transaction price to the income and total
        accumulator.income += transaction.price;
        accumulator.total += transaction.price;
      } else {
        // Otherwise, add the transaction price to the outcome and subtract it from the total
        accumulator.outcome += transaction.price;
        accumulator.total -= transaction.price;
      }
      // Return the accumulator for the next iteration
      return accumulator;
    },
    {
      // Initial value of the accumulator
      income: 0,
      outcome: 0,
      total: 0
    }
  );

  return summary;
}

In the index.tsx (Summary) add the following code to call this hook:

import { useSummary } from "../../hooks/useSummary";

const summary = useSummary(); // importing our own hook

React Hook Form

React Hook Form is a library that simplifies form handling in React applications. It leverages React hooks to manage form state, validation, and submission without the need for large boilerplate code. This results in more concise, readable, and performant code.

useForm: The Core Hook

The useForm hook provided by React Hook Form is the core of the library. It is used to handle form state and validation. Here are its main functionalities:

1. Registering Inputs:

  • useForm allows you to register input fields and keep track of their values.
const { register } = useForm();
// In your component:
<input {...register('username')} />

2. Form State Management:

  • It manages the state of the form, including the current values of all inputs.
const { handleSubmit, watch, formState: { errors } } = useForm();

3. Validation:

  • You can define validation rules directly on your inputs.
<input {...register('username', { required: true, minLength: 3 })} />

4. Form Submission:

  • handleSubmit handles form submissions and validates inputs.
const onSubmit = (data) => console.log(data);
// In your form:
<form onSubmit={handleSubmit(onSubmit)}>

5. Resetting Form:

  • The reset function allows you to reset the form values.
const { reset } = useForm();
// To reset the form:
<button onClick={() => reset()}>Reset</button>

Zod: Schema Validation Library

Zod is a TypeScript-first schema declaration and validation library. It provides a way to define the structure of data and then validate it. This can be particularly useful when combined with React Hook Form for form validation.

Using Zod with React Hook Form

1. Define Schema:

  • Create a schema using Zod to define the structure and validation rules of your form data.
import { z } from 'zod';

const schema = z.object({
  username: z.string().min(3, 'Username must be at least 3 characters long'),
  age: z.number().int().positive('Age must be a positive integer'),
});

2. Integrate with useForm:

  • Use Zod to validate form data within React Hook Form.
  • React Hook Form has a resolver that you can use to integrate with Zod.
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';

const { register, handleSubmit, formState: { errors } } = useForm({
  resolver: zodResolver(schema),
});

const onSubmit = (data) => console.log(data);

3. Form Component:

  • Register inputs and handle the form submissions.
<form onSubmit={handleSubmit(onSubmit)}>
  <input {...register('username')} />
  {errors.username && <p>{errors.username.message}</p>}

  <input type="number" {...register('age')} />
  {errors.age && <p>{errors.age.message}</p>}

  <button type="submit">Submit</button>
</form>

Install React Hook Form and Zod

npm i react-hook-form zod
npm i @hookform/resolvers

Step 1: Define the Schema

const searchFormSchema = zod.object({
  query: zod.string(),
})

Here, you define a schema using the zod library. The schema describes the shape of your form data and specifies validation rules. In this case:

  • searchFormSchema is an object schema.
  • It contains a single field, query, which must be a string.

Step 2: Infer Type from Schema

type SearchFormInputs = zod.infer<typeof searchFormSchema>;

This line uses zod to infer a TypeScript type from the searchFormSchema.

  • zod.infer<typeof searchFormSchema> generates a TypeScript type that mirrors the structure of searchFormSchema.
  • SearchFormInputs is now a type that represents an object with a single string property query.

Step 3: Create the Form Component in SearchForm

In this step, you define a React functional component named SearchForm.

1. Initialize useForm:

const { register, handleSubmit } = useForm<SearchFormInputs>({
  resolver: zodResolver(searchFormSchema)
});
  • useForm<SearchFormInputs> initializes the form and tells React Hook Form to use the SearchFormInputs type for form data.
  • resolver: zodResolver(searchFormSchema) integrates Zod schema validation with React Hook Form. The zodResolver function ensures that the form data is validated according to the searchFormSchema rules.
  • register: A function to register input fields.
  • handleSubmit: A function to handle the form submission.
import { MagnifyingGlass } from "phosphor-react";
import { SearchFormContainer } from "./styles";
import { useForm } from "react-hook-form";
import * as zod from 'zod';
import { zodResolver } from "@hookform/resolvers/zod";

// Schema
const searchFormSchema = zod.object({
  query: zod.string(),
})

// Return type of our schema in the form
type SearchFormInputs = zod.infer<typeof searchFormSchema>;

export function SearchForm() {

  const {
    register,
    handleSubmit,
    formState: { isSubmitting } } = useForm<SearchFormInputs>({
    resolver: zodResolver(searchFormSchema)
  });

  async function handleSearchTransactions(data: SearchFormInputs) {
    await new Promise(resolve => setTimeout(resolve, 2000)) // resolver a promessa apos 2 segundos com objetivo simular API
    console.log(data);
  }

  return (
    <SearchFormContainer onSubmit={handleSubmit(handleSearchTransactions)}>
      <input
        type="text"
        placeholder="Search for a transaction"
        {...register('query')}
      />
      <button disabled={isSubmitting}>
        <MagnifyingGlass size={20} />
        Search
      </button>
    </SearchFormContainer>
  )
}

Step 4: Understand Controlled Components in React-hook-form

React Hook Form has a feature called Controller. The controller is used to wrap third-party form components (not native ones) or custom components, so they can integrate smoothly with React Hook Form’s validation and state management. Essentially, it acts as a bridge between React Hook Form and non-standard form elements.

Radix Radio Group

Radix is a library of accessible UI components. In this context, we’re using Radix’s TransactionType and TransactionTypeButton components to create a radio group for selecting the type of transaction (either “income” or “outcome”).

onValueChange is an event handler called when the value changes.

import * as Dialog from '@radix-ui/react-dialog';
import { CloseButton, Content, Overlay, TransactionType, TransactionTypeButton } from './styles';
import { ArrowCircleDown, ArrowCircleUp, X } from 'phosphor-react';
import * as zod from 'zod';
import { zodResolver } from "@hookform/resolvers/zod";
import { Controller, useForm } from 'react-hook-form';

// Schema
const newTransactionFormSchema = zod.object({
  description: zod.string(),
  price: zod.number(),
  category: zod.string(),
  type: zod.enum(['income', 'outcome'])
})

type NewTransactionFormInputs = zod.infer<typeof newTransactionFormSchema>;

export default function NewTransactionModal() {

  const {
    control,
    register,
    handleSubmit,
    formState: {
      isSubmitting
    }
  } = useForm<NewTransactionFormInputs>({
    resolver: zodResolver(newTransactionFormSchema),
    defaultValues: {
      type: 'income'
    },
  })

  async function handleCreateNewTransaction(data: NewTransactionFormInputs) {
    await new Promise(resolve => setTimeout(resolve, 2000)) // resolver a promessa apos 2 segundos com objetivo simular API

    console.log(data)
  }

  return (
    <Dialog.Portal>
      <Overlay/>
      <Content>
        <Dialog.Title>New Transaction</Dialog.Title>
        <CloseButton>
          <X size={24}/>
        </CloseButton>

        <form onSubmit={handleSubmit(handleCreateNewTransaction)}>
          <input
            type="text"
            placeholder='Description'
            required
            {... register('description')}
          />
          <input
            type="number"
            placeholder='Price'
            required
            {... register('price', {valueAsNumber: true})}
          />
          <input
            type="text"
            placeholder='Category'
            required
            {... register('category')}
          />

          <Controller
            control={control}
            name='type'
            render={({field}) => {
              return (
                <TransactionType onValueChange={field.onChange} value={field.value}>
                  <TransactionTypeButton variant='income' value='income'>
                    <ArrowCircleUp size={24} />
                    Income
                  </TransactionTypeButton>
                  <TransactionTypeButton variant='outcome' value='outcome'>
                    <ArrowCircleDown size={24} />
                    Outcome
                  </TransactionTypeButton>
                </TransactionType>
              )
            }}
          />

          <button type='submit' disabled={isSubmitting}>
            Register
          </button>
        </form>

      </Content>
    </Dialog.Portal>
  )
}

In summary:

  • React Hook Form’s Controller helps integrate non-standard form components.
  • Radix’s TransactionType and TransactionTypeButton create an accessible and styled radio group.
  • The Controller component ensures that this radio group correctly reads and updates the form state managed by React Hook Form.

Code explanations:

  • control={control}: This prop connects the Controller to the form state managed by React Hook Form. The control object is provided by the useForm hook.
  • name='type': This is the name of the form field, in this case, ‘type’. It corresponds to the value in the form state.
  • render={({ field }) => { ... }: The render prop takes a function that receives field-related props (like field). These props help integrate the custom component with the React Hook Form.
  • onValueChange={field.onChange}: This ensures that when a radio button is selected, the form state is updated accordingly.
  • value={field.value}: This sets the current value of the radio group based on the form state.

Step 5: Searching the transactions

Introduction to Axios

Axios is a popular JavaScript library used to make HTTP requests from a browser or Node.js. It simplifies the process of interacting with APIs and handling responses. Axios is often preferred over the native fetch API because it offers a cleaner syntax and additional features like request and response interceptors, automatic JSON transformation, and better error handling.

1. Install AXIOS library:

$ npm install axios

2. Create a new folder called lib and inside add a file called axios.ts.

3. Code Explanation:

const response = await api.get('transactions', {
  params: {
    q: query
  }
});

api.get:

  • This is a method from the axios instance (api) to make a GET request. GET is an HTTP method used to request data from a server.

'transactions':

  • This is the endpoint (or path) to which the GET request is being made. It typically represents a specific resource on your server. In this case, it’s /transactions.

{ params: { q: query } }:

  • This is an options object passed to the get method.
  • params: This key is used to define query parameters for the request.
  • q: query: This sets the query parameter q to the value of the query variable. Query parameters are often used to filter or search data on the server. For example, if query is "income", the request URL might look like this: http://yourapi.com/transactions?q=income.

await:

  • This keyword is used to wait for the promise returned by api.get to resolve. It allows you to write asynchronous code that looks synchronous, making it easier to read and maintain. The await a keyword can only be used inside an async function.

const response:

  • The result of the await api.get(...) the call is assigned to the response variable. This response the object contains various properties, including data, which holds the data returned from the API.

Axios.ts file:

import axios from "axios";

export const api = axios.create({
  baseURL: 'http://localhost:3000',
});

TransactionsContext file:

import { createContext, ReactNode, useEffect, useState } from "react";
import { api } from "../lib/axios";

// @types
interface Transaction {
  id: number,
  description: string;
  type: 'income' | 'outcome';
  price: number;
  category: string;
  createdAt: string;
}
interface TransactionContextType {
  transactions: Transaction[]; // list of transactions
  fetchTransactions: (query?: string) =>  Promise<void>;
}
interface TransactionsProviderProps {
  children: ReactNode; // any element valid in React
}

// Creating the Context
export const TransactionsContext = createContext({} as TransactionContextType)

// Export the Context.Provider
export function TransactionsProvider({ children }: TransactionsProviderProps) {
  const [transactions, setTransactions] = useState<Transaction[]>([])

  async function fetchTransactions(query?: string) {
    const response = await api.get('transactions', {
      params: {
        _sort: 'createdAt',
        _order: 'desc',
        q: query
      }
    })

    setTransactions(response.data);
  }

  useEffect(() => { // useEffect can not be async
    fetchTransactions();
  }, [])

  // Return Code
  return (
    <TransactionsContext.Provider value={{ transactions, fetchTransactions }}>
      {children}
    </TransactionsContext.Provider>
  )
}

Creating a New Transaction

To ensure our functionality is not tied to a specific modal or dialog component, we will create the function to add new transactions within the context. This approach prevents potential issues that could arise from coupling our functionality with a single modal or dialog.

Additionally, we have created a new interface for the function’s props that mirrors the structure of the dialog component’s props, maintaining consistency and flexibility.

Breaking It Down

1. Function Declaration:

  • async function createTransaction(data: CreateTransactionInput): This declares an asynchronous function named createTransaction that takes a single argument data of type CreateTransactionInput.

2. Destructuring the data Object:

  • const { description, price, category, type } = data;: This line extracts the description, price, category, and type properties from the data object and assigns them to corresponding variables.

3. Making the POST Request:

  • const response = await api.post(‘transactions’, { … }): This line uses axios (through an instance named api) to send an HTTP POST request to the transactions endpoint.
  • The payload of the POST request includes the description, price, category, type, and createdAt (which is set to the current date and time).

4. Updating State:

  • setTransactions(state => [response.data, …state]);: This line updates the transactions state by adding the newly created transaction (returned from the API as response.data) to the beginning of the current state array.
  • setTransactions is likely a state setter function from a useState hook.
  • The state parameter represents the current state, and [response.data, …state] creates a new array with the new transaction at the front, followed by the existing transactions.

Putting It All Together

Here’s a step-by-step explanation of what happens when createTransaction is called:

1. Receive Data:

  • The function receives a data object that includes details about the transaction to be created (description, price, category, and type).

2. Send POST Request:

  • The function constructs a payload for the POST request, including the transaction details and the current date (createdAt).
  • It then sends the POST request to the transactions endpoint using axios.

3. Handle Response:

  • The function waits for the response from the server (await), which contains the newly created transaction data.

4. Update State:

  • The function updates the local state of transactions by prepending the new transaction to the existing list of transactions.

Updated TransactionsContext.ts file:

import { createContext, ReactNode, useEffect, useState } from "react";
import { api } from "../lib/axios";

// @types
interface Transaction {
  id: number,
  description: string;
  type: 'income' | 'outcome';
  price: number;
  category: string;
  createdAt: string;
}
interface CreateTransactionInput {
  description: string;
  price: number;
  category: string;
  type: 'income' | 'outcome';
}
interface TransactionContextType {
  transactions: Transaction[]; // list of transactions
  fetchTransactions: (query?: string) => Promise<void>;
  createTransaction: (data: CreateTransactionInput) => Promise<void>;
}
interface TransactionsProviderProps {
  children: ReactNode; // any element valid in React
}

// Creating the Context
export const TransactionsContext = createContext({} as TransactionContextType)

// Export the Context.Provider
export function TransactionsProvider({ children }: TransactionsProviderProps) {
  const [transactions, setTransactions] = useState<Transaction[]>([])

  async function fetchTransactions(query?: string) {
    const response = await api.get('transactions', {
      params: {
        _sort: 'createdAt',
        _order: 'desc',
        q: query
      }
    })

    setTransactions(response.data);
  }

  async function createTransaction(data: CreateTransactionInput) {
    const { description, price, category, type } = data;

    const response = await api.post('transactions', {
      description,
      price,
      category,
      type,
      createdAt: new Date(),
    })

    setTransactions(state => [response.data, ...state]);
  }

  useEffect(() => { // useEffect can not be async
    fetchTransactions();
  }, [])

  // Return Code
  return (
    <TransactionsContext.Provider value={{ transactions, fetchTransactions, createTransaction }}>
      {children}
    </TransactionsContext.Provider>
  )
}

Updated NewTransactionModal

import * as Dialog from '@radix-ui/react-dialog';
import { CloseButton, Content, Overlay, TransactionType, TransactionTypeButton } from './styles';
import { ArrowCircleDown, ArrowCircleUp, X } from 'phosphor-react';
import * as zod from 'zod';
import { zodResolver } from "@hookform/resolvers/zod";
import { Controller, useForm } from 'react-hook-form';
import { api } from '../../lib/axios';
import { useContext } from 'react';
import { TransactionsContext } from '../../contexts/TransactionsContext';

// Schema
const newTransactionFormSchema = zod.object({
  description: zod.string(),
  price: zod.number(),
  category: zod.string(),
  type: zod.enum(['income', 'outcome'])
})

type NewTransactionFormInputs = zod.infer<typeof newTransactionFormSchema>;

export default function NewTransactionModal() {

  const {createTransaction} = useContext(TransactionsContext)

  const {
    control,
    register,
    handleSubmit,
    reset,
    formState: {
      isSubmitting
    }
  } = useForm<NewTransactionFormInputs>({
    resolver: zodResolver(newTransactionFormSchema),
    defaultValues: {
      type: 'income'
    },
  })

  async function handleCreateNewTransaction(data: NewTransactionFormInputs) {
    //await new Promise(resolve => setTimeout(resolve, 2000)) // resolver a promessa apos 2 segundos com objetivo simular API
    const { description, price, category, type } = data;

    await createTransaction({
      description,
      price,
      category,
      type
    });

    reset();
  }

  return (
    <Dialog.Portal>
      <Overlay/>
      <Content>
        <Dialog.Title>New Transaction</Dialog.Title>
        <CloseButton>
          <X size={24}/>
        </CloseButton>

        <form onSubmit={handleSubmit(handleCreateNewTransaction)}>
          <input
            type="text"
            placeholder='Description'
            required
            {... register('description')}
          />
          <input
            type="number"
            placeholder='Price'
            required
            {... register('price', {valueAsNumber: true})}
          />
          <input
            type="text"
            placeholder='Category'
            required
            {... register('category')}
          />

          <Controller
            control={control}
            name='type'
            render={({field}) => {
              return (
                <TransactionType onValueChange={field.onChange} value={field.value}>
                  <TransactionTypeButton variant='income' value='income'>
                    <ArrowCircleUp size={24} />
                    Income
                  </TransactionTypeButton>
                  <TransactionTypeButton variant='outcome' value='outcome'>
                    <ArrowCircleDown size={24} />
                    Outcome
                  </TransactionTypeButton>
                </TransactionType>
              )
            }}
          />

          <button type='submit' disabled={isSubmitting}>
            Register
          </button>
        </form>

      </Content>
    </Dialog.Portal>
  )
}

Fixing ESLINT errors

Install ESLINT from Rockeseat

npm i eslint @rocketseat/eslint-config -D

Create a file in the root of the project: eslintrc.json

Run in the terminal:

npx eslint src --ext .tsx, .ts

Performance

Install a library to avoid rendering child components in the context all the time, improving performance in the application.

npm install use-context-selector react scheduler
import { createContext } from "use-context-selector";

// Creating the Context
export const TransactionsContext = createContext({} as TransactionContextType)

Now, we need to change all useContext hooks to useContextSelector as shown below:

const transactions = useContextSelector(TransactionsContext, (context) => {
    return context.transactions
});
  

When a context value is changed, all components that useContext will re-render.

To solve this issue, useContextSelector is proposed and later proposed Speculative Mode with context selector support. This library provides the API in userland.

useCallback

Is a React hook that returns a memorized version of the callback function that only changes if one of the dependencies has changed. This is useful to prevent unnecessary re-renders of re-creations of functions when a component re-renders.

const createTransaction = useCallback(
    async (data: CreateTransactionInput) => {
      const { description, price, category, type } = data;
      const response = await api.post('transactions', {
        description,
        price,
        category,
        type,
        createdAt: new Date(),
      })
      setTransactions(state => [response.data, ...state]);
    }, [],
  )
  • Function Signature: This is an asynchronous function that takes an object of type CreateTransactionInput as its argument.
  • Destructuring: The properties description, price, category, and type are extracted from the data object.
  • API Call: An HTTP POST request is made to the ‘transactions’ endpoint with the extracted properties and a timestamp (createdAt: new Date()).
  • State Update: The response from the API call, which presumably contains the new transaction data, is used to update the state. The setTransactions function is called with a callback that prepends the new transaction (response.data) to the current state (state).
  • The empty array [] indicates that this callback function does not depend on any external values from the component’s scope. As a result, the createTransaction function will be created only once and won’t change on subsequent renders unless the component unmounts and remounts.

Why use useCallback here?

Using useCallback ensures that createTransaction maintains a stable reference between renders. This can be beneficial in several scenarios:

  • Avoid function to be re-created in memory when you don’t need that to happen.
  • Performance Optimization: Prevents unnecessary re-creation of the function, which can be costly if done frequently.
  • Passing as Props: If createTransaction is passed to child components, using useCallback ensures that those children don’t re-render unnecessarily because the function reference didn’t change.

Add the code to the fetchTransactions function

const fetchTransactions = useCallback(
    async (query?: string) => {
      const response = await api.get('transactions', {
        params: {
          _sort: 'createdAt',
          _order: 'desc',
          q: query
        }
      })
      setTransactions(response.data);
    }, [],
 )

React MEMO

memo is a higher-order component that you wrap around your component. It tells React to only re-render the component if its props OR hooks have changed. If the props stay the same, React will skip the re-render, saving time and resources.

Por que que um component renderiza?
- Hooks changed (mudou estado, contexto, reducer)
- Props changes (mudou propriedades)
- Parent rerendered (component pai renderizou)

Qual o fluxo de renderizacao?
1. O React recria o HTML da interface daquele componente
2. Compara a versao do HTML recriada com a versao anterior
3. SE mudou alguma coisa, ele reescreve o HTML na tela

Memo:
0. Hooks changed, Props changed (deep comparison)
0.1: Comparar com a versao anterior dos hooks e this.props
0.2: SE mudou algo, ele vai permitir a nova renderizacao SE NAO mudar nada nao entra no fluxo acima

This is how you can use the MEMO below:

import React, { memo } from 'react';

const MyComponent = (props) => {
  // Component logic and JSX here
  return <div>{props.name}</div>;
};

export default memo(MyComponent);

useMemo Hook

The useMemo hook in React is used to optimize performance by memoizing expensive calculations. It ensures that a function only recalculates a value when one of its dependencies changes, thus avoiding unnecessary computations on every render.

import React, { useMemo } from 'react';

const MyComponent = ({ number }) => {
  // Expensive calculation
  const squaredNumber = useMemo(() => {
    console.log('Calculating...');
    return number * number;
  }, [number]); // Dependencies array

  return <div>Squared Number: {squaredNumber}</div>;
};

export default MyComponent;
  • Expensive Calculation: Suppose you have a calculation that squares a number, which could be more complex in real scenarios.
  • useMemo Hook: Wrap this calculation inside useMemo. Pass the function that performs the calculation as the first argument.
  • Dependencies Array: The second argument is an array of dependencies. useMemo will only recalculate the value if one of these dependencies changes. If number changes, useMemo will recalculate the squared number. Otherwise, it will use the memoized value from the previous render.

Note that in our application the summary variable will only be re-created when transactions change:

// {income: 0, outcome: 0, total: 0} => reduzir o array (transactions) a uma nova estrutura de dados
  const summary = useMemo(() => {
    transactions.reduce(
      (accumulator, transaction) => {
        // Check if the transaction is of type 'income'
        if (transaction.type === 'income') {
          // Add the transaction price to the income and total
          accumulator.income += transaction.price;
          accumulator.total += transaction.price;
        } else {
          // Otherwise, add the transaction price to the outcome and subtract it from the total
          accumulator.outcome += transaction.price;
          accumulator.total -= transaction.price;
        }
        // Return the accumulator for the next iteration
        return accumulator;
      },
      {
        // Initial value of the accumulator
        income: 0,
        outcome: 0,
        total: 0
      }
    );
  },[transactions]
  )

GitHub Link

Link: https://github.com/ruivergani/dt-money

Similar Articles

Check similiar articles below 🚀