You are currently viewing React.js

React.js

Table of Contents

Getting started with React

Check Node.js version and update it to the latest version:

				
					node -v
				
			

If you don’t have Node.js installed on your device, go to node.js website, download and install the latest version. After that recheck the version and go to the next step.

Install first app with vite, choose a name for your project (it will be your app installation folder), select the framework and a variant by moving the arrow keys up/down, and finally hit the enter key.

				
					cd Desktop
npm create vite@latest

✔ Project name: … react-app
✔ Select a framework: › React
✔ Select a variant: › TypeScript

Done. Now run:
    cd react-app
    npm install
				
			

Run code below to open the app inside Visual Studio Code:

				
					code .
				
			

If this command doesn’t work in your terminal please follow the following solution:

Open the Command Palette via P and type shell command to find the Shell Command:

Use the Uninstall ‘code’ command in the PATH command before the “Install ‘code’ command in PATH” command.

Source: https://stackoverflow.com/questions/29955500/code-is-not-working-in-on-the-command-line-for-visual-studio-code-on-os-x-ma

Open internal VS Code terminal by these shortkeys: control + ~

Inside the terminal run the app:

				
					npm run dev

You will see something like this:
VITE v4.3.0 ready in 770 ms

    ➜ Local: http://localhost:5173/
    ➜ Network: use --host to expose
    ➜ press h to show help
				
			

Open the local address in your browser (the port might be different in your machine, it doesn’t matter. Just open it)

You’ll see a page with these contents:

Building Components

In React, a component cannot return more than one element. Three solutions to solve the issue:

  1. Wrap all the elements in one parent element like <div></div>
				
					function ListGroup() {
  return (
    <div>
      <h1>List</h1>
      <ul className="list-group">
        <li className="list-group-item">An item</li>
        <li className="list-group-item">A second item</li>
        <li className="list-group-item">A third item</li>
        <li className="list-group-item">A fourth item</li>
        <li className="list-group-item">And a fifth one</li>
      </ul>
    </div>
  );
}

export default ListGroup;
				
			
  1. Wrap them in a <Fragment></Fragment> elements by importing it. (Recommended in order not to have any additional DOM elements in your final code)
				
					import { Fragment } from "react";

function ListGroup() {
  return (
    <Fragment>
      <h1>List</h1>
      <ul className="list-group">
        <li className="list-group-item">An item</li>
        <li className="list-group-item">A second item</li>
        <li className="list-group-item">A third item</li>
        <li className="list-group-item">A fourth item</li>
        <li className="list-group-item">And a fifth one</li>
      </ul>
    </Fragment>
  );
}

export default ListGroup;
				
			
  1. Wrap them in an empty angle bracket set <> and removing the import line code. This means a Fragment. (Also recommended in order not to have any additional DOM elements in your final code)
				
					function ListGroup() {
  return (
    <>
      <h1>List</h1>
      <ul className="list-group">
        <li className="list-group-item">An item</li>
        <li className="list-group-item">A second item</li>
        <li className="list-group-item">A third item</li>
        <li className="list-group-item">A fourth item</li>
        <li className="list-group-item">And a fifth one</li>
      </ul>
    </>
  );
}

export default ListGroup;
				
			
  1. To have a dynamic list instead of a boring static list, we can define a list of items and use mapping to convert them to list items:
				
					const items = ["New York", "San Francisco", "Tokyo", "London", "Tehran"];

function ListGroup() {
  return (
    <>
      <h1>List</h1>
      <ul className="list-group">
        {items.map((item) => (
          <li key={item} className="list-group-item">
            {item}
          </li>
        ))}
      </ul>
    </>
  );
}

export default ListGroup;
				
			

We added key={item} to the code to prevent the following warning in the browsers’ console log:

Warning: Each child in a list should have a unique “key” prop.

Styling Components

Shortcodes

  • Cmd+P (Mac) or Ctrl+P (Windows)
    Search for the files in your project
  • Cmd+D (Mac) or Ctrl+D (Windows)
    To select the next occurrence of the selected keyword.
  • Cmd+Shift+P (Mac) or Ctrl+Shift+P (Windows)
    To open the command palette.

Useful VSCode Extensions for React.js

Install Bootstrap

				
					npm i bootstrap@latest
				
			
  • Clean the app.css file located in src folder
  • Delete index.css file
  • Go to main.csx file and change the line 4:
				
					import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './index.css'
				
			

to this:

				
					import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import "bootstrap/dist/css/bootstrap.css";
				
			

Done! Bootstrap is now installed on your app.

  • Create a folder in src folder and name it components.
  • Put all your components’ files inside this folder to well organize your app components

Fragment

To use multiple DOM elements in your code you need to use Fragment:

				
					import { Fragment } from "react";

function ListGroup() {
  return (
    <Fragment>
      ... Your code inside Fragment is here.
    </Fragment>
  );
}

export default ListGroup;
				
			

or use empty angle brackets (No need to import Fragment in this method):

				
					function ListGroup() {
  return (
    <>
      ... Your code inside Fragment is here.
    </>
  );
}

export default ListGroup;
				
			

React Icons

Installation:

				
					npm i react-icons@latest
				
			

The website to check and search the list of icons:

https://react-icons.github.io/react-icons

Search the icons and copy the name of the icon you want. Then import it:
(The name of the icon I have selected is BsFillCalendarCheckFill)

				
					import { BsFillCalendarCheckFill } from 'react-icons/bs';
				
			

Because the icon name is started with Bs, we add the bs at the end of the address:
‘react-icons/bs

				
					import { BsFillCalendarCheckFill } from "react-icons/bs";

function App() {
  return (
    <div>
      <BsFillCalendarCheckFill></BsFillCalendarCheckFill>
    </div>
  );
}

export default App;

				
			

Managin Component State

Updating Nested Objects

				
					import { useState } from "react";

function App() {
  const [drink, setDrink] = useState({
    title: "Americano",
    price: {
      withSugar: 5,
      withoutSugar: 3,
    },
  });

  const handleClick = () => {
    setDrink({
      ...drink,
      price: {
        ...drink.price,
        withSugar: drink.price.withSugar + 5,
        withoutSugar: 5,
      },
    });
  };

  return (
    <div>
      <button onClick={handleClick}>Click to order</button>
      <h1>{drink.title}</h1>
      <h2>Price List:</h2>
      <ul>
        <li>With Sugar: ${drink.price.withSugar}</li>
        <li>Without Sugar: ${drink.price.withoutSugar}</li>
      </ul>
    </div>
  );
}

export default App;

				
			

Updating Arrays

				
					const [tags, setTags] = useState(["happy", "cheerful"]);

  const handleClick = () => {
    // Add
    setTags([...tags, "exciting"]);

    // Remove
    setTags(tags.filter((tag) => tag !== "happy"));

    // Update
    setTags(tags.map((tag) => (tag === "happy" ? "happiness" : tag)));
  };
				
			

Updating Array of Objects

				
					import { useState } from "react";

function App() {
  const [bugs, setBugs] = useState([
    { id: 1, title: "Bug 1", fixed: false },
    { id: 2, title: "Bug 2", fixed: false },
  ]);

  const handleClick = (bugId: number) => {
    setBugs(
      bugs.map((bug) =>
        bug.id === bugId ? { ...bug, fixed: !bug.fixed } : bug
      )
    );
  };

  const bugsDataItems = bugs.map((bug) => (
    <li key={bug.id}>
      <span>{bug.id}: </span>
      <strong>{bug.title} </strong>
      <span>
        Status: <strong>{String(bug.fixed)}</strong>
      </span>
      <button onClick={() => handleClick(bug.id)}>
        Click to {String(!bug.fixed)}
      </button>
    </li>
  ));

  return (
    <div>
      <ul>{bugsDataItems}</ul>
    </div>
  );
}

export default App;

				
			

Simplifying Update Logic with Immer

Installing Immer

				
					npm i immer@9.0.19
				
			
				
					import { useState } from "react";
import produce from "immer";

function App() {
  const [bugs, setBugs] = useState([
    { id: 1, title: "Bug 1", fixed: false },
    { id: 2, title: "Bug 2", fixed: false },
  ]);

  const handleClick = (bugId: number) => {
    // setBugs(
    //   bugs.map((bug) =>
    //     bug.id === bugId ? { ...bug, fixed: !bug.fixed } : bug
    //   )
    // );
    
    // Draft: A proxy object that record the changes, we're going to apply to the bugs array. So Imagine draft is a copy of the bugs array. So we're free to mutate or modify just like your regular JavaScript object.
    setBugs(
      produce((draft) => {
        const bug = draft.find((bug) => bug.id === 1);
        if (bug) bug.fixed = true;
      })
    );
  };

  return (
    <div>
      {bugs.map((bug) => (
        <p key={bug.id}>
          {bug.title} {bug.fixed ? "Fixed" : "New"}
        </p>
      ))}
      <button onClick={handleClick}>Click Me</button>
    </div>
  );
}

export default App;
				
			

Sharing States between Components

NavBar.tsx

				
					import React from "react";

interface Props {
  cartItemsCount: number;
}

const NavBar = ({ cartItemsCount }: Props) => {
  return <div>NavBar: {cartItemsCount}</div>;
};

export default NavBar;

				
			

Cart.tsx

				
					import React from "react";

interface Props {
  cartItems: string[];
  onClear: () => void;
}

function Cart({ cartItems, onClear }: Props) {
  return (
    <>
      <div>Cart</div>
      <ul>
        {cartItems.map((item) => (
          <li key={item}>{item}</li>
        ))}
      </ul>
      <button onClick={onClear}>Clear</button>
    </>
  );
}

export default Cart;

				
			

App.tsx

				
					import { useState } from "react";
import NavBar from "./components/NavBar";
import Cart from "./components/cart";

function App() {
  const [cartItems, setCartItems] = useState([
    "Product1",
    "Product2",
    "Product3",
  ]);

  return (
    <div>
      <NavBar cartItemsCount={cartItems.length} />
      <Cart cartItems={cartItems} onClear={() => setCartItems([])}></Cart>
    </div>
  );
}

export default App;

				
			

Expandable Text

ExpandableText.tsx

				
					import React, { useState } from "react";

interface Props {
  children: string;
  maxChars?: number;
}

const ExpandableText = ({ maxChars = 100, children }: Props) => {
  const [isExpanded, setExpanded] = useState(false);

  if (children.length <= 100) return <p>{children}</p>;

  const text = isExpanded ? children : children.substring(0, maxChars);
  return (
    <p>
      {text}...
      <button onClick={() => setExpanded(!isExpanded)}>
        {isExpanded ? "Less..." : "More..."}
      </button>
    </p>
  );
};

export default ExpandableText;
				
			

App.tsx

				
					import ExpandableText from "./components/ExpandableText";

function App() {
  return (
    <div>
      <ExpandableText>
        Lorem ipsum dolor sit amet consectetur adipisicing elit. Delectus soluta
        animi consectetur pariatur! Vero illum sunt eligendi quis officia porro
        repellendus! Eius sed aspernatur voluptatum reprehenderit officiis
        mollitia laboriosam eos cupiditate, vitae, excepturi quisquam provident?
        Voluptatum culpa molestiae ea minima quidem consectetur harum, facilis
        qui sapiente ad quo, nobis, tempore porro enim explicabo dolor amet
        impedit libero officiis laboriosam quasi sunt ullam distinctio maiores.
        Modi a id deserunt quas ipsum ipsam optio necessitatibus cumque tempore
        accusantium. Ullam officia quidem corrupti unde minus possimus numquam
        est incidunt laborum velit. Eveniet dolores officiis magnam aperiam
        blanditiis assumenda delectus perspiciatis ex tempora aut?
      </ExpandableText>
    </div>
  );
}

export default App;
				
			

Building Forms

Form.tsx

				
					import React, { FormEvent, useRef, useState } from "react";


const Form = () => {

  const [person, setPerson] = useState({
    name: "",
    age: "",
  });

  const handleSubmit = (event: FormEvent) => {
    event.preventDefault();
    console.log(person);
  };

  return (
    <form onSubmit={handleSubmit}>
      <div className="mb-3">
        <label htmlFor="name" className="form-label">
          Name
        </label>
        <input
          value={person.name}
          onChange={(event) =>
            setPerson({ ...person, name: event.target.value })
          }
          id="name"
          className="form-control"
        ></input>
      </div>
      <div className="mb-3">
        <label htmlFor="age" className="form-label">
          Age
        </label>
        <input
          value={person.age}
          onChange={(event) =>
            setPerson({ ...person, age: parseInt(event.target.value) })
          }
          id="age"
          type="number"
          className="form-control"
        />
      </div>
      <button className="btn btn-primary" type="submit">
        Submit
      </button>
    </form>
  );
};

export default Form;

				
			

React Hook Form

				
					npm i react-hook-form@7.43
				
			
				
					import { useForm } from "react-hook-form";
				
			
				
					import React, { FormEvent, useRef, useState } from "react";
import { FieldValue, FieldValues, useForm } from "react-hook-form";

const Form = () => {
  const { register, handleSubmit } = useForm();
  const onSubmit = (data: FieldValues) => console.log(data);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div className="mb-3">
        <label htmlFor="name" className="form-label">
          Name
        </label>
        <input {...register("name")} id="name" className="form-control"></input>
      </div>
      <div className="mb-3">
        <label htmlFor="age" className="form-label">
          Age
        </label>
        <input
          {...register("age")}
          id="age"
          type="number"
          className="form-control"
        />
      </div>
      <button className="btn btn-primary" type="submit">
        Submit
      </button>
    </form>
  );
};

export default Form;

				
			

Appying Validation

				
					import { FieldValue, FieldValues, useForm } from "react-hook-form";

// The TypeScript is not aware of our input fields. To solve this and provide a better development experience, we can use an interface to define the shape of the form.
interface formData {
  name: string;
  age: number;
}

const Form = () => {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<formData>();
  const onSubmit = (data: FieldValues) => console.log(data);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div className="mb-3">
        <label htmlFor="name" className="form-label">
          Name
        </label>
        <input
          {...register("name", { minLength: 3, required: true })}
          id="name"
          className="form-control"
        ></input>
        {/* The question mark and period is called "Optional Chaning" in JavaScript. The reason why we need question mark here is that our error object can be empty. So if you don't have a name property, and then we try to access the type property, we're going to get a runtime error. To prevent it we use optional chaning. So the expression "errors.name?.type" evaluated only if we have a property called name in the errors object. Otherwise, it's ignored.*/}
        {errors.name?.type === "required" && (
          <p className="text-danger">This field is required.</p>
        )}
        {errors.name?.type === "minLength" && (
          <p className="text-danger">Name should be at least 3 characters.</p>
        )}
      </div>
      <div className="mb-3">
        <label htmlFor="age" className="form-label">
          Age
        </label>
        <input
          {...register("age")}
          id="age"
          type="number"
          className="form-control"
        />
      </div>
      <button className="btn btn-primary" type="submit">
        Submit
      </button>
    </form>
  );
};

export default Form;

				
			

Schema based Validation with Zod

				
					npm i zod@3.20.6
				
			
				
					import { z } from "zod";
				
			
				
					npm i @hookform/resolvers@2.9.11
				
			
				
					import { zodResolver } from "@hookform/resolvers/zod";
				
			
				
					import { FieldValues, useForm } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";

const schema = z.object({
  name: z.string().min(3, { message: "Name must be at least 3 characters" }),
  age: z
    .number({ invalid_type_error: "Age field is required!" })
    .min(18, { message: "18+ only" }),
});

type FormData = z.infer<typeof schema>;

const Form = () => {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<FormData>({ resolver: zodResolver(schema) });
  const onSubmit = (data: FieldValues) => console.log(data);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div className="mb-3">
        <label htmlFor="name" className="form-label">
          Name
        </label>
        <input {...register("name")} id="name" className="form-control"></input>
        {errors.name && <p className="text-danger">{errors.name.message}</p>}
      </div>
      <div className="mb-3">
        <label htmlFor="age" className="form-label">
          Age
        </label>
        <input
          {...register("age", { valueAsNumber: true })}
          id="age"
          type="number"
          className="form-control"
        />
        {errors.age && <p className="text-danger">{errors.age.message}</p>}
      </div>
      <button className="btn btn-primary" type="submit">
        Submit
      </button>
    </form>
  );
};

export default Form;

				
			
				
					import { FieldValues, useForm } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";

const schema = z.object({
  name: z.string().min(3, { message: "Name must be at least 3 characters" }),
  age: z
    .number({ invalid_type_error: "Age field is required!" })
    .min(18, { message: "18+ only" }),
});

type FormData = z.infer<typeof schema>;

const Form = () => {
  const {
    register,
    handleSubmit,
    formState: { errors, isValid },
  } = useForm<FormData>({ resolver: zodResolver(schema) });
  const onSubmit = (data: FieldValues) => console.log(data);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div className="mb-3">
        <label htmlFor="name" className="form-label">
          Name
        </label>
        <input {...register("name")} id="name" className="form-control"></input>
        {errors.name && <p className="text-danger">{errors.name.message}</p>}
      </div>
      <div className="mb-3">
        <label htmlFor="age" className="form-label">
          Age
        </label>
        <input
          {...register("age", { valueAsNumber: true })}
          id="age"
          type="number"
          className="form-control"
        />
        {errors.age && <p className="text-danger">{errors.age.message}</p>}
      </div>
      <button disabled={!isValid} className="btn btn-primary" type="submit">
        Submit
      </button>
    </form>
  );
};

export default Form;
				
			

Small Project: Expense Tracker

ExpenseList.tsx

				
					interface Expense {
  id: number;
  description: string;
  amount: number;
  category: string;
}

interface Props {
  expenses: Expense[];
  onDelete: (id: number) => void;
}

const ExpenseList = ({ expenses, onDelete }: Props) => {
  if (expenses.length === 0) return null;

  return (
    <div>
      <table className="table table-striped">
        <thead>
          <tr>
            <th>Description</th>
            <th>Amount</th>
            <th>Category</th>
            <th>Actions</th>
          </tr>
        </thead>
        <tbody>
          {expenses.map((expense) => (
            <tr key={expense.id}>
              <td>{expense.description}</td>
              <td>{expense.amount}</td>
              <td>{expense.category}</td>
              <td>
                <button
                  className="btn btn-outline-danger"
                  onClick={() => onDelete(expense.id)}
                >
                  Delete
                </button>
              </td>
            </tr>
          ))}
        </tbody>
        <tfoot>
          <tr>
            <td>Total</td>
            {/* In JavaScript all arrays have a reduce method that we can use to combine all elements in the array into something else. For example, here, we can combine all expnense objects into a number that represents the total. The way we do this is by passing an arrow function with two parameters here. So the first parameter is acc that is short for accumulator. This is like a variable that holds the total amount. The second parameter is an expense object. So this arrow function has two parameters. Now in the body of this arrow function, we need to get the amount of the current expense and add it to the accumulator. So the accumulator is just a number that holds the total value. So the reduce method iterates over our array of expenses, and in each iteration, it runs this arrow function, it takes the amount of the current expense and adds it to the accumulator. Now as the second argument to the reduce method, we need to pass the initial value for the accumulator, we're going to set that to zero. So this returns a number. So after we call the reduce method, we call the toFixed method. This is available on all nubmers in JavaScript. And here we pass 2, to specify the number of digits after the decimal point. */}
            <td>
              $
              {expenses
                .reduce((acc, expense) => expense.amount + acc, 0)
                .toFixed(2)}
            </td>
            <td></td>
            <td></td>
          </tr>
        </tfoot>
      </table>
    </div>
  );
};

export default ExpenseList;

				
			

ExpenseFilter.tsx

				
					import categories from "../categories";

interface Props {
  onSelectCategories: (category: string) => void;
}
const ExpenseFilter = ({ onSelectCategories }: Props) => {
  return (
    <select
      className="form-select"
      onChange={(event) => onSelectCategories(event.target.value)}
    >
      <option value="">All categories</option>
      {categories.map((category) => (
        <option key={category} value={category}>
          {category}
        </option>
      ))}
    </select>
  );
};

export default ExpenseFilter;

				
			

ExpenseForm.tsx

				
					import { z } from "zod";
import { FormState, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import categories from "../categories";

const schema = z.object({
  description: z
    .string()
    .min(3, { message: "Description should be at least 3 characters." })
    .max(50),
  amount: z
    .number({ invalid_type_error: "Amount is required." })
    .min(0.01)
    .max(100_000),
  // enum means one of manny values
  category: z.enum(categories, {
    errorMap: () => ({ message: "Category is required." }),
  }),
});

type ExpenseFormData = z.infer<typeof schema>;

interface Props {
  onSubmit: (data: ExpenseFormData) => void;
}

const ExpenseForm = ({ onSubmit }: Props) => {
  const {
    register,
    handleSubmit,
    reset,
    formState: { errors },
  } = useForm<ExpenseFormData>({ resolver: zodResolver(schema) });

  return (
    <form
      onSubmit={handleSubmit((data) => {
        onSubmit(data);
        reset();
      })}
    >
      <div className="mb-3">
        <label htmlFor="description" className="form-label">
          Description
        </label>
        <input
          {...register("description")}
          id="description"
          type="text"
          className="form-control"
        />
        {errors.description && (
          <p className="text-danger">{errors.description.message}</p>
        )}
      </div>
      <div className="mb-3">
        <label htmlFor="amount" className="form-label">
          Amount
        </label>
        <input
          {...register("amount", { valueAsNumber: true })}
          id="amount"
          type="number"
          className="form-control"
        />
        {errors.amount && (
          <p className="text-danger">{errors.amount.message}</p>
        )}
      </div>
      <div className="mb-3">
        <label htmlFor="category" className="form-label">
          Category
        </label>
        <select {...register("category")} id="category" className="form-select">
          <option>Select a category</option>
          {categories.map((category) => (
            <option key={category} value={category}>
              {category}
            </option>
          ))}
        </select>
        {errors.category && (
          <p className="text-danger">{errors.category.message}</p>
        )}
      </div>
      <button className="btn btn-primary">Submit</button>
    </form>
  );
};

export default ExpenseForm;

				
			

categories.ts

				
					const categories = ["Groceries", "Utilities", "Entertainments"] as const;

export default categories;
				
			

App.tsx

				
					import "bootstrap/dist/css/bootstrap.css";
import "./index.css";
import ExpenseList from "./expense-tracker/components/ExpenseList";
import { useState } from "react";
import ExpenseFilter from "./expense-tracker/components/ExpenseFilter";
import ExpenseForm from "./expense-tracker/components/ExpenseForm";
import categories from "./expense-tracker/categories";

function App() {
  // State variable representing the selected filter. When that filter changes, the app component will render and the list of expenses will be updated.
  const [selectedCategory, setSelectedCategory] = useState("");
  const [expenses, setExpenses] = useState([
    { id: 1, description: "Milk", amount: 2, category: "Groceries" },
    { id: 2, description: "Electricity", amount: 1, category: "Utilities" },
    {
      id: 3,
      description: "Gym Membership",
      amount: 4,
      category: "Entertainments",
    },
    { id: 4, description: "Clothes", amount: 6, category: "Entertainments" },
  ]);

  // If selected category is truthy, if it has a value, then we need to call expenses.filter, so want to return all expenses in the given category. so e.category should be equal to the selected category, otherwise if selected category is not truthy, meaning if it's an empty string, which means the user hasn't selected the category, then we should set visible expenses to expenses.
  // The expense variable holds all the expenses. visibleExpenses holds only the expenses the user is going to see based on the selected filter.
  const visibleExpenses = selectedCategory
    ? expenses.filter((e) => e.category === selectedCategory)
    : expenses;

  return (
    <div>
      <div className="mb-5">
        <ExpenseForm
          onSubmit={(expense) =>
            setExpenses([...expenses, { ...expense, id: expenses.length + 1 }])
          }
        />
      </div>
      <div className="mb-3">
        <ExpenseFilter
          onSelectCategories={(category) => setSelectedCategory(category)}
        />
      </div>
      <ExpenseList
        expenses={visibleExpenses}
        // Return all expenses except the one with the given ID
        onDelete={(id) => setExpenses(expenses.filter((e) => e.id !== id))}
      />
    </div>
  );
}

export default App;

				
			

Connecting to the Backend

Effect Hook

Our React components should be pure functions, a pure function should not have any side effects and should return the same result if we give it the same input.

Now to make our functions or components pure, we have to keep any code that makes changes outside of the render phase. But there are situations where we might need to store some data in the local storage of the browser, so we can remember it in the future. Or we may need to call a server to fetch or save the data, or maybe we want to manually modify DOM elements. And none of these situations are about rendering a component, they have nothing to do with returning some JSX markup. So where can we implement them, that’s where the effect hook comes in.

With the effect hook, we can tell React to execute a piece of code after a component is rendered.

				
					import "bootstrap/dist/css/bootstrap.css";
import "./index.css";
import { useRef } from "react";

const App = () => {
  // To get a reference to the input element, we use the useRef, initialize it with null, the type of the target element is HTMLInputElement. So we call this and get a reference object.
  const ref = useRef<HTMLInputElement>(null);

  // We check that if ref.current is defined, then we call the ref.current.focus(). This piece of code has nothing to do with returning some JSX markup. With this piece of code, we're changing the state of the DOM.
  // So this piece of code has a side effect is changing something outside of this component. So our component is no longer a pure component.
  // Side effect
  if (ref.current) ref.current.focus();

  return (
    <div>
      <input ref={ref} type="text" className="form-control" />
    </div>
  );
};

export default App;

				
			

To make this a pure component, we can use the Effect Hook.

				
					import "bootstrap/dist/css/bootstrap.css";
import "./index.css";
import { useEffect, useRef } from "react";

const App = () => {
  const ref = useRef<HTMLInputElement>(null);

  // Afrer Render
  // The function we pass here will be called after each render. And this is an opportunity for us to write a piece of code that causes side effect, we can store something in the local storage, which works with a DOM elements, we can call the server and swap.
  useEffect(() => {
    // Side effect
    if (ref.current) ref.current.focus();
  });

  useEffect(() => {
    document.title = "My App";
  });

  return (
    <div>
      <input ref={ref} type="text" className="form-control" />
    </div>
  );
};

export default App;

				
			

Just like the state and ref hooks, we can only call the Effect Hook at the top level of our component. So we cannot call it inside loops or if statements, and also similar to the ref and state hooks, we can call this multiple times for different purposes.

For example, right after, we can have another effect.

When we have multiple effects, React will run them in order after each render.

Effect Hook Dependencies

App.tsx

				
					import "bootstrap/dist/css/bootstrap.css";
import "./index.css";
import ProductList from "./components/ProductList";
import { useState } from "react";

function App() {
  const [categories, setCategories] = useState("");
  return (
    <>
      <div className="mb-3">
        <select
          name=""
          id=""
          className="form-select"
          onChange={(event) => setCategories(event.target.value)}
        >
          <option value=""></option>
          <option value="Clothing">Clothing</option>
          <option value="Household">Household</option>
        </select>
      </div>
      <div className="mb-3">
        <ProductList category={categories} />
      </div>
    </>
  );
}

export default App;

				
			

ProductList.tsx

				
					import React, { useEffect, useState } from "react";

const ProductList = ({ category }: { category: string }) => {
  const [products, setProducts] = useState<string[]>([]);

  useEffect(() => {
    category
      ? console.log("Fetching products in", category)
      : console.log("Fetching List");
    setProducts(["Clothings", "Accessories"]);
  }, [category]);

  return <div>ProductList</div>;
};

export default ProductList;

				
			

Effect Clean Up

To provide clean up code, we return a function, and in this function, we call our disconnect function. So the function that we pass to the effect hook can optionally return a function for cleaning up. This is not always necessary, but if we need to do cleanup, this is the way we do it.

Generally speaking our cleanup function should stop or undo whatever the effect was doing. For expanple, if we’re connecting or subscribing to something, our cleanup function would unsubscribe or disconnect. As another example, if we’re showing a modal here, our cleanup function should hide the modal. Or if we are fetching some data from server here, our cleanup function should either abort the fetch or ignore the result.

				
					import "bootstrap/dist/css/bootstrap.css";
import "./index.css";
import { useEffect } from "react";

const connect = () => console.log("Connectiong...");
const disconnect = () => console.log("Disconnectiong...");

function App() {
  useEffect(() => {
    document.title = "Hello";
    connect();
    // To provide clean up code, we return a function, and in this function, we call our disconnect function. So the function that we pass to the effect hook can optionally return a function for cleaning up. This is not always necessary, but if we need to do cleanup, this is the way we do it.
    // Generally speaking our cleanup function should stop or undo whatever the effect was doing. For expanple, if we're connecting or subscribing to something, our cleanup function whould unsubscribe or disconnect. As another example, if we're showing a modal here, our cleanup function should hide the modal. Or if we are fetching some data from server here, our cleanup function should either abort the fetch or ignore the result.
    return () => disconnect();
  });
  return <div></div>;
}

export default App;

				
			

We have learned that in the development mode with a strict mode turn on, React renders each component twice. So in this case, React renders our app component. That’s why we see the connecting… message. But before React renders our app component the second time, first it is going to remove the component from the screen. This is called unmounting. So just like we can mount a painting on a wall, React, mounts our components on the screen and unmount them when they are no longer needed.

So with the strict mode enabled, before React mounts our component for the second time, first it has to unmount it. That is why our clean up code is executed.

Fetching Data

To sending HTTP requests to the server we can use:

  • The fetch() function that is implemented in all modern browsers,
  • and also a library called axios.
				
					npm i axios@1.3.4
				
			
				
					import axios from "axios";
				
			

Promise is an object that holds the eventual result or failure of an asynchronous(Long running) operation.

				
					import axios from "axios";
				
			
				
					import "bootstrap/dist/css/bootstrap.css";
import "./index.css";
import axios from "axios";
import { useEffect, useState } from "react";

// What if you want to print the name of the first user, if you access the first element is this array, and then try to access the name propery? Look, we don't have auto completion. This is where we can use TypeScript to add auto completion and type safety to our code. So we don't have access invalid properties.
// we use an interface to define the shape of our user objects. Now you'll see, each user object has properies like ID, name, username, and so on. Let's say we're only interested in the first two properties. So here, wee're saying each user has an ID, that is a number, and a name, that is a string. We don't have to type out all those user going to use them.
interface User {
  id: number;
  name: string;
}

function App() {
  // In line 22 we have a compilation error (res.data), because we initialize the users contant with an empty array. So the TypeScript complier doesn't know the type of objects we're going to store. Here we're going to store numbers, strings, objects, or what. So once again, we need to type in angle brackets and specify the type of this array.
  const [users, setUsers] = useState<User[]>([]);

  useEffect(() => {
    // All promises have a method called then, that is a callback to execute when the promise is resolved.
    axios
      // Now that we have the User interface, when calling the get method to an angle brackets, we specify the type of data we're going to fetch that is user array. And with that, when we type a period after data[0]., we get auto completion
      .get<User[]>("https://jsonplaceholder.typicode.com/users")
      .then((res) => setUsers(res.data));
    // Because we have set the state in useEffect, we have to add an empty array as the dependency to this effect. This is very important, Always remember to add this because otherwise, you'll end up in an infinite loop and it will constantly hit the server sending hundreds or thousands of requests.
  }, []);
  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

export default App;

				
			

Error handling

				
					import "bootstrap/dist/css/bootstrap.css";
import "./index.css";
import axios from "axios";
import { useEffect, useState } from "react";

interface User {
  id: number;
  name: string;
}

function App() {
  const [users, setUsers] = useState<User[]>([]);
  const [error, setError] = useState("");

  useEffect(() => {
    // get -> promise -> res / err
    axios
      .get<User[]>("https://jsonplaceholder.typicode.com/users")
      .then((res) => {
        setUsers(res.data);
        setError("");
      })
      // Handling errors
      .catch((err) => setError(err.message));
  }, []);

  // Display only the error message
  if (error) {
    return <p className="text-danger">{error}</p>;
  }
  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

export default App;
				
			

Working with Async and Await

				
					import "bootstrap/dist/css/bootstrap.css";
import "./index.css";
import axios, { AxiosError } from "axios";
import { useEffect, useState } from "react";

interface User {
  id: number;
  name: string;
}

function App() {
  const [users, setUsers] = useState<User[]>([]);
  const [error, setError] = useState("");

  useEffect(() => {
    const fetchUsers = async () => {
      try {
        const res = await axios.get<User[]>(
          "https://jsonplaceholder.typicode.com/users"
        );
        setUsers(res.data);
        setError("");
      } catch (err) {
        // Type annotation is not allowed in catch clause. So we wrap this error object in a paranthesis and us the "as" keyword to tell the TypeScript compiler the type of this object
        setError((err as AxiosError).message);
      }
    };
    fetchUsers();
  }, []);

  if (error) {
    return <p className="text-danger">{error}</p>;
  }
  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

export default App;
				
			

Cancelling a Fetch Request

				
					import "bootstrap/dist/css/bootstrap.css";
import "./index.css";
import axios, { CanceledError } from "axios";
import { useEffect, useState } from "react";

interface User {
  id: number;
  name: string;
}

function App() {
  const [users, setUsers] = useState<User[]>([]);
  const [error, setError] = useState("");

  useEffect(() => {
    const controller = new AbortController();

    axios
      .get<User[]>("https://jsonplaceholder.typicode.com/users", {
        signal: controller.signal,
      })
      .then((res) => {
        setUsers(res.data);
        // setError("");
      })
      .catch((err) => {
        if (err instanceof CanceledError) return;
        setError(err.message);
      });

    return () => controller.abort();
  }, []);

  return (
    <>
      <p className="text-danger">{error}</p>
      <ul>
        {users.map((user) => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </>
  );
}

export default App;
				
			

Showing a Loading Indicator

				
					import "bootstrap/dist/css/bootstrap.css";
import "./index.css";
import axios, { CanceledError } from "axios";
import { useEffect, useState } from "react";

interface User {
  id: number;
  name: string;
}

function App() {
  const [users, setUsers] = useState<User[]>([]);
  const [error, setError] = useState("");
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    const controller = new AbortController();

    setIsLoading(true);
    axios
      .get<User[]>("https://jsonplaceholder.typicode.com/users", {
        signal: controller.signal,
      })
      .then((res) => {
        setUsers(res.data);
        setIsLoading(false);
      })
      .catch((err) => {
        if (err instanceof CanceledError) return;
        setError(err.message);
        setIsLoading(false);
      });
      // This doesn't work with strict mode turned on
      // .finally(() => {
      //   setIsLoading(true);
      // });

    return () => controller.abort();
  }, []);

  return (
    <>
      <p className="text-danger">{error}</p>
      {isLoading && <div className="spinner-border text-primary"></div>}
      <ul>
        {users.map((user) => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </>
  );
}

export default App;
				
			

Deleting Data

				
					import "bootstrap/dist/css/bootstrap.css";
import "./index.css";
import axios, { CanceledError } from "axios";
import { useEffect, useState } from "react";

interface User {
  id: number;
  name: string;
}

function App() {
  const [users, setUsers] = useState<User[]>([]);
  const [error, setError] = useState("");
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    const controller = new AbortController();

    setIsLoading(true);
    axios
      .get<User[]>("https://jsonplaceholder.typicode.com/users", {
        signal: controller.signal,
      })
      .then((res) => {
        setUsers(res.data);
        setIsLoading(false);
      })
      .catch((err) => {
        if (err instanceof CanceledError) return;
        setError(err.message);
        setIsLoading(false);
      });

    return () => controller.abort();
  }, []);

  // The constant returns a function that takes the user object of type User that we have defined at line 6
  const deleteUser = (user: User) => {
    const originalUsers = [...users];
    // Optimistic approch:
    // 1. update UI first
    // Filter users and get all the users(u.id) except the one with the given ID(user.id)
    setUsers(users.filter((u) => u.id !== user.id));

    // 2. Call the server to persist the changes
    axios
      // This returns a promise, but in this case, there is nothing we want to do after the promises resolved.
      // So we don't want to call '.then', instead we just want to call the '.catch' method to catch potential errors.
      .delete("https://jsonplaceholder.typicode.com/users/" + user.id)
      .catch((err) => {
        setError(err.message);
        // If it catches an error, it restores the UI back to the original state.
        setUsers(originalUsers);
      });
  };

  return (
    <>
      <p className="text-danger">{error}</p>
      {isLoading && (
        <div className="spinner-grow text-primary">
          <span className="visually-hidden">Loading...</span>
        </div>
      )}
      <ul className="list-group">
        {users.map((user) => (
          <li
            key={user.id}
            className="list-group-item d-flex justify-content-between"
          >
            {user.name}
            <button
              className="btn btn-outline-danger"
              onClick={() => {
                console.log(user.id, "deleted");
                // Pass the user object that we're currently rendering
                deleteUser(user);
              }}
            >
              Delete
            </button>
          </li>
        ))}
      </ul>
    </>
  );
}

export default App;
				
			

Optimistic Update: Update the UI quickly to give the user instant feedback, and then we’ll call the server to persist the changes.

  • Update the UI
  • Call the server

With this approach, our UI will be blazing fast.

Pessimistic Update: We assume that the call to the server is going fail. So first, we call the server and wait for the result. If the call is successful, then we’ll update the UI.

  • Call the server
  • Update the UI

With this approach, our UI is going to feel a little bit slow.

Creating Data

				
					import "bootstrap/dist/css/bootstrap.css";
import "./index.css";
import axios, { CanceledError } from "axios";
import { useEffect, useState } from "react";

interface User {
  id: number;
  name: string;
}

function App() {
  const [users, setUsers] = useState<User[]>([]);
  const [error, setError] = useState("");
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    const controller = new AbortController();

    setIsLoading(true);
    axios
      .get<User[]>("https://jsonplaceholder.typicode.com/users", {
        signal: controller.signal,
      })
      .then((res) => {
        setUsers(res.data);
        setIsLoading(false);
      })
      .catch((err) => {
        if (err instanceof CanceledError) return;
        setError(err.message);
        setIsLoading(false);
      });

    return () => controller.abort();
  }, []);

  // The constant returns a function that takes the user object of type User that we have defined at line 6
  const deleteUser = (user: User) => {
    const originalUsers = [...users];
    // Optimistic approch:
    // 1. update UI first
    // Filter users and get all the users(u.id) except the one with the given ID(user.id)
    setUsers(users.filter((u) => u.id !== user.id));

    // 2. Call the server to persist the changes
    axios
      // This returns a promise, but in this case, there is nothing we want to do after the promises resolved.
      // So we don't want to call '.then', instead we just want to call the '.catch' method to catch potential errors.
      .delete("https://jsonplaceholder.typicode.com/users/" + user.id)
      .catch((err) => {
        setError(err.message);
        // If it catches an error, it restores the UI back to the original state.
        setUsers(originalUsers);
      });
  };

  const addUser = () => {
    const originalUsers = [...users];
    const newUser = { id: 0, name: "Shazdeh" };
    setUsers([newUser, ...users]);

    axios
      // As a second argument we use newUser
      .post("https://jsonplaceholder.typicode.com/users", newUser)
      // one solution is to write the code as the following line:
      // .then((res) => setUsers([res.data, ...users]))
      // Another solution to make the code more readable is as follow:
      // Because we need to access the data property, we can destructure the response and grab the data property right here.
      // So first we wrap 'res' in parenthesis, then we add braces and grab the {data} property.
      // What is {data}?
      // .then(({data}) => setUsers([data, ...users]))
      // We can rename this property by typing a colon to savedUser. This is just an alias that we're assigning to the propery. So now we can use the savedUser object instead of data in this part: setUsers([data, ...users]))
      .then(({ data: savedUser }) => setUsers([savedUser, ...users]))
      .catch((err) => {
        setError(err.message);
        setUsers(originalUsers);
      });
  };

  return (
    <>
      <p className="text-danger">{error}</p>
      {isLoading && (
        <div className="spinner-grow text-primary">
          <span className="visually-hidden">Loading...</span>
        </div>
      )}
      <button className="btn btn-primary mb-3" onClick={addUser}>
        Add a user
      </button>
      <ul className="list-group">
        {users.map((user) => (
          <li
            key={user.id}
            className="list-group-item d-flex justify-content-between"
          >
            {user.name}
            <button
              className="btn btn-outline-danger"
              onClick={() => {
                console.log(user.id, "deleted");
                // Pass the user object that we're currently rendering
                deleteUser(user);
              }}
            >
              Delete
            </button>
          </li>
        ))}
      </ul>
    </>
  );
}

export default App;
				
			

Updating Data

				
					import "bootstrap/dist/css/bootstrap.css";
import "./index.css";
import axios, { CanceledError } from "axios";
import { useEffect, useState } from "react";

interface User {
  id: number;
  name: string;
}

function App() {
  const [users, setUsers] = useState<User[]>([]);
  const [error, setError] = useState("");
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    const controller = new AbortController();

    setIsLoading(true);
    axios
      .get<User[]>("https://jsonplaceholder.typicode.com/users", {
        signal: controller.signal,
      })
      .then((res) => {
        setUsers(res.data);
        setIsLoading(false);
      })
      .catch((err) => {
        if (err instanceof CanceledError) return;
        setError(err.message);
        setIsLoading(false);
      });

    return () => controller.abort();
  }, []);

  // The constant returns a function that takes the user object of type User that we have defined at line 6
  const deleteUser = (user: User) => {
    const originalUsers = [...users];
    // Optimistic approch:
    // 1. update UI first
    // Filter users and get all the users(u.id) except the one with the given ID(user.id)
    setUsers(users.filter((u) => u.id !== user.id));

    // 2. Call the server to persist the changes
    axios
      // This returns a promise, but in this case, there is nothing we want to do after the promises resolved.
      // So we don't want to call '.then', instead we just want to call the '.catch' method to catch potential errors.
      .delete("https://jsonplaceholder.typicode.com/users/" + user.id)
      .catch((err) => {
        setError(err.message);
        // If it catches an error, it restores the UI back to the original state.
        setUsers(originalUsers);
      });
  };

  const addUser = () => {
    const originalUsers = [...users];
    const newUser = { id: 0, name: "Shazdeh" };
    setUsers([newUser, ...users]);

    axios
      // As a second argument we use newUser
      .post("https://jsonplaceholder.typicode.com/users", newUser)
      // one solution is to write the code as the following line:
      // .then((res) => setUsers([res.data, ...users]))
      // Another solution to make the code more readable is as follow:
      // Because we need to access the data property, we can destructure the response and grab the data property right here.
      // So first we wrap 'res' in parenthesis, then we add braces and grab the {data} property.
      // What is {data}?
      // .then(({data}) => setUsers([data, ...users]))
      // We can rename this property by typing a colon to savedUser. This is just an alias that we're assigning to the propery. So now we can use the savedUser object instead of data in this part: setUsers([data, ...users]))
      .then(({ data: savedUser }) => setUsers([savedUser, ...users]))
      .catch((err) => {
        setError(err.message);
        setUsers(originalUsers);
      });
  };

  const updateUser = (user: User) => {
    const originalUsers = [...users];
    console.log("User updated", user.name + "!");
    const updatedUser = { ...user, name: user.name + "!" };
    // Because we're updating an object, we should map the users
    // If the ID of the current user equals the ID of the user that is passed to this function, then we return the updated user, otherwise, we return the same user object.
    setUsers(users.map((u) => (u.id === user.id ? updatedUser : u)));

    // We can use 'patch' or 'put' method.
    // In HTTP, we use the PUT method for replacing an object and the patch method for patching or updating one or more of its properties.
    // In this case we're going to update only a single propery, so we're going to use the patch method.
    axios
      .patch(
        "https://jsonplaceholder.typicode.com/users/" + user.id,
        updatedUser
      )
      .catch((err) => {
        setError(err.message);
        setUsers(originalUsers);
      });
  };

  return (
    <>
      <p className="text-danger">{error}</p>
      {isLoading && (
        <div className="spinner-grow text-primary">
          <span className="visually-hidden">Loading...</span>
        </div>
      )}
      <button className="btn btn-primary mb-3" onClick={addUser}>
        Add a user
      </button>
      <ul className="list-group">
        {users.map((user) => (
          <li
            key={user.id}
            className="list-group-item d-flex justify-content-between"
          >
            {user.name}
            <div>
              <button
                className="btn btn-secondary mx-1"
                onClick={() => updateUser(user)}
              >
                Update
              </button>
              <button
                className="btn btn-outline-danger"
                onClick={() => {
                  console.log(user.id, "deleted");
                  // Pass the user object that we're currently rendering
                  deleteUser(user);
                }}
              >
                Delete
              </button>
            </div>
          </li>
        ))}
      </ul>
    </>
  );
}

export default App;

				
			

Extracting a Reusable API Client

api-client.ts

./services/api-clients

				
					import axios, {CanceledError} from "axios";

export default axios.create( {
  baseURL: 'https://jsonplaceholder.typicode.com'
})

export {CanceledError}
				
			

App.tsx

				
					import "bootstrap/dist/css/bootstrap.css";
import "./index.css";
import apiClients, { CanceledError } from "./services/api-clients";
import { useEffect, useState } from "react";

interface User {
  id: number;
  name: string;
}

function App() {
  const [users, setUsers] = useState<User[]>([]);
  const [error, setError] = useState("");
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    const controller = new AbortController();

    setIsLoading(true);
    apiClients
      .get<User[]>("/users", {
        signal: controller.signal,
      })
      .then((res) => {
        setUsers(res.data);
        setIsLoading(false);
      })
      .catch((err) => {
        if (err instanceof CanceledError) return;
        setError(err.message);
        setIsLoading(false);
      });

    return () => controller.abort();
  }, []);

  const deleteUser = (user: User) => {
    const originalUsers = [...users];
    setUsers(users.filter((u) => u.id !== user.id));

    apiClients.delete("/users/" + user.id).catch((err) => {
      setError(err.message);
      setUsers(originalUsers);
    });
  };

  const addUser = () => {
    const originalUsers = [...users];
    const newUser = { id: 0, name: "Shazdeh" };
    setUsers([newUser, ...users]);

    apiClients
      .post("/users", newUser)
      .then(({ data: savedUser }) => setUsers([savedUser, ...users]))
      .catch((err) => {
        setError(err.message);
        setUsers(originalUsers);
      });
  };

  const updateUser = (user: User) => {
    const originalUsers = [...users];
    console.log("User updated", user.name + "!");
    const updatedUser = { ...user, name: user.name + "!" };
    setUsers(users.map((u) => (u.id === user.id ? updatedUser : u)));

    apiClients.patch("/users/" + user.id, updatedUser).catch((err) => {
      setError(err.message);
      setUsers(originalUsers);
    });
  };

  return (
    <>
      <p className="text-danger">{error}</p>
      {isLoading && (
        <div className="spinner-grow text-primary">
          <span className="visually-hidden">Loading...</span>
        </div>
      )}
      <button className="btn btn-primary mb-3" onClick={addUser}>
        Add a user
      </button>
      <ul className="list-group">
        {users.map((user) => (
          <li
            key={user.id}
            className="list-group-item d-flex justify-content-between"
          >
            {user.name}
            <div>
              <button
                className="btn btn-secondary mx-1"
                onClick={() => updateUser(user)}
              >
                Update
              </button>
              <button
                className="btn btn-outline-danger"
                onClick={() => {
                  console.log(user.id, "deleted");
                  deleteUser(user);
                }}
              >
                Delete
              </button>
            </div>
          </li>
        ))}
      </ul>
    </>
  );
}

export default App;
				
			

Extracting the User Service

Our App component is a little bit too concerned with making HTTP requests. For example it knows about the AbortController, which is purely about HTTP. It’s about canceling requests. Our component also knows about the request methods like GET, POST, PATCH, and DELETE. It also knows about our endpoint. And this endpoint is repeated in many different places in this component.

So our components, like a chef was also responsible for shopping the ingredients. Typically, at a restaurant, a chef should not be concerned with shopping, they should only focus on the primary responsibility. By the same token, our components should only focus on their primary responsibility, which is returning some markup and handling user interactions at a high level.

So to improve this code, we should extract all the logic around making HTTP requests into a separate service. This allows us to separate concerns and make our code more modular and reusable. Potentially, we can reuse that service in other components. If somewhere else, we need to fetch the list of users, we can reuse our service, we don’t need repeat all this code somewhere else.

App.tsx

				
					import "bootstrap/dist/css/bootstrap.css";
import "./index.css";
import { CanceledError } from "./services/api-clients";
import { useEffect, useState } from "react";
import userService, { User } from "./services/user-service";

// interface User {
//   id: number;
//   name: string;
// }

function App() {
  const [users, setUsers] = useState<User[]>([]);
  const [error, setError] = useState("");
  const [isLoading, setIsLoading] = useState(false);

  // With these changes, our Effect hook knows absolutely nothing about making HTTP requests. So we have a better separation of concerns.
  useEffect(() => {
    // const controller = new AbortController();

    setIsLoading(true);
    // apiClients
    //   .get<User[]>("/users", {
    //     signal: controller.signal,
    //   })

    // Instead of apiClients, we're going to csll user service
    // userService.getAllUsers()
    // we get an object. Destructure the object to grab the request and the cancel function.
    const { request, cancel } = userService.getAllUsers();

    // We don't have to get worry about passing a signal, we simply tell the service, give me all the users. This returns a promis
    // signal: controller.signal,
    request
      .then((res) => {
        setUsers(res.data);
        setIsLoading(false);
      })
      .catch((err) => {
        if (err instanceof CanceledError) return;
        setError(err.message);
        setIsLoading(false);
      });

    // In this implementation, we need access to this controller object. Now this controller is purely about making HTTP requests. We don't want to export this from our user service module. This is part of the implementation detail.
    // As a metaphor, think of a remote control. A remote control has a complex logic board on the inside, but as a user, we don't have to worry about that complexity. We simply work with buttons on the outside. Thise buttons hide the implementation detail.
    // To solve the controller.abort() compilation issue, instead of returning the promise from the method: return apiClient.get<User[]>... we're going to store it in an object called request.
    // return () => controller.abort();
    // instead of controller.abort() we call the cancel function
    return () => cancel();
  }, []);

  const deleteUser = (user: User) => {
    const originalUsers = [...users];
    setUsers(users.filter((u) => u.id !== user.id));

    userService.deleteUser(user.id).catch((err) => {
      setError(err.message);
      setUsers(originalUsers);
    });
  };

  const addUser = () => {
    const originalUsers = [...users];
    const newUser = { id: 0, name: "Shazdeh" };
    setUsers([newUser, ...users]);

    userService
      .createUser(newUser)
      .then(({ data: savedUser }) => setUsers([savedUser, ...users]))
      .catch((err) => {
        setError(err.message);
        setUsers(originalUsers);
      });
  };

  const updateUser = (user: User) => {
    const originalUsers = [...users];
    console.log("User updated", user.name + "!");
    const updatedUser = { ...user, name: user.name + "!" };
    setUsers(users.map((u) => (u.id === user.id ? updatedUser : u)));

    userService.updateUser(updatedUser).catch((err) => {
      setError(err.message);
      setUsers(originalUsers);
    });
  };

  return (
    <>
      <p className="text-danger">{error}</p>
      {isLoading && (
        <div className="spinner-grow text-primary">
          <span className="visually-hidden">Loading...</span>
        </div>
      )}
      <button className="btn btn-primary mb-3" onClick={addUser}>
        Add a user
      </button>
      <ul className="list-group">
        {users.map((user) => (
          <li
            key={user.id}
            className="list-group-item d-flex justify-content-between"
          >
            {user.name}
            <div>
              <button
                className="btn btn-secondary mx-1"
                onClick={() => updateUser(user)}
              >
                Update
              </button>
              <button
                className="btn btn-outline-danger"
                onClick={() => {
                  console.log(user.id, "deleted");
                  deleteUser(user);
                }}
              >
                Delete
              </button>
            </div>
          </li>
        ))}
      </ul>
    </>
  );
}

export default App;

				
			

user-service.ts

				
					import apiClients from "./api-clients";

// Because we're going to use this interface in our app component, we need to export this from this module.
export interface User {
  id: number;
  name: string;
}

// Create a class called UserService
// In this class we're going to have methods for getting all the users, creating a user, updating a user and deleting a user.
class UserService {
  // Add a method called getAllUsers()
  // In this method we're going to have the logic for sending an HTTP request to our backend.
  getAllUsers() {
    const controller = new AbortController();

    // We send a GET request to our users endpoint and this returns a promise that we can return this promise right away.
    // return apiClients    
    const request = apiClients
      .get<User[]>("/users", {
        signal: controller.signal,
      })
      // Return an object with two properties: 1. the request object 2. the Cancel method. We set this to a function and in this function we call controller.abort()
      // So the consumer of the UserService simply uses the cancel method or the cancel button to cancel a request.
      // How it's happened internally, it's irrelevant. That's implementation detail.
      return { request, cancel: () => controller.abort() }
  }

  // Our service needs to expose a method or deleting a user.
  deleteUser(id: number) {
    return apiClients.delete("/users/" + id);
  }
  createUser(user: User) {
    return apiClients.post("/users", user);
  }
  updateUser(user: User) {
    return apiClients.patch("/users/" + user.id, user);
  }
}

// Export a new instance of this class as a default object
export default new UserService();
				
			

Creating a Generic HTTP Service

user-service.ts

				
					import create from "./http-service";

export interface User {
  id: number;
  name: string;
}

export default create('/users');
				
			

http-service.ts

				
					import apiClients from "./api-clients";

interface Entity {
  id: number;
}

class HttpService {
  // T in this context is called a generic type parameter, It is a placeholder for type. So when calling this method, will supply a generic type argument, like getAll<User> or getAll<Post>
  // Two approches for defining the generic endpoint:
  // 1. To add the endpoint as a parameter in the paranthesis: getAll<T>(endpoint: string)
  // The problem of this approach is that the consumer of this class has to provide the endpoint. That means in our component, we have to pass the users endpoint, that's something we want to avoid.
  // 2. In the HttpService , we add a propery called endpoint of type string.

  endpoint: string;

  // Then we create a constructor, which is a function that is called when we create an instance of this class, we give the constructor a parameter called an endpoint of type string.
  constructor(endpoint: string) {
    // And, we initialize the endpoint property with the endpoint parameter.
    this.endpoint = endpoint;
  }

  getAll<T>() {
    const controller = new AbortController();

    const request = apiClients
    // We replace "/users" with this.endpoint
      .get<T[]>(this.endpoint, {
        signal: controller.signal,
      })
      return { request, cancel: () => controller.abort() }
  }

  delete(id: number) {
    return apiClients.delete(this.endpoint + "/" + id);
  }
  create<T>(entity: T) {
    return apiClients.post(this.endpoint, entity);
  }

  // 
  update<T extends Entity>(entity: T) {
    // Our compiler doesn't know that our entities, which are instances of type T, have a property called ID.
    // To solve this problem, we need to add a constraint to this type. We need to tell the TypeScript compiler that obejcts of type T should have a property called ID.
    // The way we do this is using an interface. Go to the top.
    return apiClients.patch(this.endpoint + "/" + entity.id, entity);
  }
}

// We don't want to create a new instance of of this class, because here we have to pass an endpoint, we don't want to hard code an endpoint like /users because with this our HTTP service will no longer be reusable:
// export default new HttpService('/users');
// Instead of exporting an instance of this class, we should export a function for creating an instance of this class.
// We'll give it a parameter called endpoint of type string. And here we return a new HttpService with this endpoint.
const create = (endpoint: string) => new HttpService(endpoint);
// Now we export this function from this module.
export default create;
				
			

App.tsx

				
					import "bootstrap/dist/css/bootstrap.css";
import "./index.css";
import { CanceledError } from "./services/api-clients";
import { useEffect, useState } from "react";
import userService, { User } from "./services/user-service";

function App() {
  const [users, setUsers] = useState<User[]>([]);
  const [error, setError] = useState("");
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    setIsLoading(true);
    const { request, cancel } = userService.getAll<User>();

    request
      .then((res) => {
        setUsers(res.data);
        setIsLoading(false);
      })
      .catch((err) => {
        if (err instanceof CanceledError) return;
        setError(err.message);
        setIsLoading(false);
      });

    return () => cancel();
  }, []);

  const deleteUser = (user: User) => {
    const originalUsers = [...users];
    setUsers(users.filter((u) => u.id !== user.id));

    userService.delete(user.id).catch((err) => {
      setError(err.message);
      setUsers(originalUsers);
    });
  };

  const addUser = () => {
    const originalUsers = [...users];
    const newUser = { id: 0, name: "Shazdeh" };
    setUsers([newUser, ...users]);

    userService
      .create(newUser)
      .then(({ data: savedUser }) => setUsers([savedUser, ...users]))
      .catch((err) => {
        setError(err.message);
        setUsers(originalUsers);
      });
  };

  const updateUser = (user: User) => {
    const originalUsers = [...users];
    console.log("User updated", user.name + "!");
    const updatedUser = { ...user, name: user.name + "!" };
    setUsers(users.map((u) => (u.id === user.id ? updatedUser : u)));

    userService.update(updatedUser).catch((err) => {
      setError(err.message);
      setUsers(originalUsers);
    });
  };

  return (
    <>
      <p className="text-danger">{error}</p>
      {isLoading && (
        <div className="spinner-grow text-primary">
          <span className="visually-hidden">Loading...</span>
        </div>
      )}
      <button className="btn btn-primary mb-3" onClick={addUser}>
        Add a user
      </button>
      <ul className="list-group">
        {users.map((user) => (
          <li
            key={user.id}
            className="list-group-item d-flex justify-content-between"
          >
            {user.name}
            <div>
              <button
                className="btn btn-secondary mx-1"
                onClick={() => updateUser(user)}
              >
                Update
              </button>
              <button
                className="btn btn-outline-danger"
                onClick={() => {
                  console.log(user.id, "deleted");
                  deleteUser(user);
                }}
              >
                Delete
              </button>
            </div>
          </li>
        ))}
      </ul>
    </>
  );
}

export default App;
				
			

Creating a Custom Data Service

We use a custom hook to share the functionality across different components. A hook is just a Function. So we can move all the logic, we’re going to reuse across components into a custom hook, or a custom function.

Crate a folder inside “src” folder called “hooks”. Inside the “hooks” folder add a new file called “useUsers”

In this component, we’re using a custom hook to fetch the list of users. If you have another component where we need to show the list of users perhaps in a drop down list, we can simply reuse this custom hook to fetch all the users, as well as the potential errors and the loading state.

This is the benefit of custom hooks. Using a custom hook, we can share functionality across different components.

useUsers.ts

				
					import { useEffect, useState } from "react";
import userService, { User } from "../services/user-service";
import { CanceledError } from "../services/api-clients";

// Define a function called useUsers
// In this function we need to return our state variables, so they can be used in our component.
const useUsers = () => {
  const [users, setUsers] = useState<User[]>([]);
  const [error, setError] = useState("");
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    setIsLoading(true);
    const { request, cancel } = userService.getAll<User>();

    request
      .then((res) => {
        setUsers(res.data);
        setIsLoading(false);
      })
      .catch((err) => {
        if (err instanceof CanceledError) return;
        setError(err.message);
        setIsLoading(false);
      });

    return () => cancel();
  }, []);

  // we return an object with three properties
  return { users, error, isLoading, setUsers, setError };
}

// Export it from this module
export default useUsers;
				
			

App.tsx

				
					import "bootstrap/dist/css/bootstrap.css";
import "./index.css";
import { CanceledError } from "./services/api-clients";
import { useEffect, useState } from "react";
import userService, { User } from "./services/user-service";
import useUsers from "./hooks/useUsers";

function App() {
  // useUsers() returns an object. We can destructure it to grab the list of the properties.
  const { users, error, isLoading, setUsers, setError } = useUsers();

  const deleteUser = (user: User) => {
    const originalUsers = [...users];
    setUsers(users.filter((u) => u.id !== user.id));

    userService.delete(user.id).catch((err) => {
      setError(err.message);
      setUsers(originalUsers);
    });
  };

  const addUser = () => {
    const originalUsers = [...users];
    const newUser = { id: 0, name: "Shazdeh" };
    setUsers([newUser, ...users]);

    userService
      .create(newUser)
      .then(({ data: savedUser }) => setUsers([savedUser, ...users]))
      .catch((err) => {
        setError(err.message);
        setUsers(originalUsers);
      });
  };

  const updateUser = (user: User) => {
    const originalUsers = [...users];
    console.log("User updated", user.name + "!");
    const updatedUser = { ...user, name: user.name + "!" };
    setUsers(users.map((u) => (u.id === user.id ? updatedUser : u)));

    userService.update(updatedUser).catch((err) => {
      setError(err.message);
      setUsers(originalUsers);
    });
  };

  return (
    <>
      <p className="text-danger">{error}</p>
      {isLoading && (
        <div className="spinner-grow text-primary">
          <span className="visually-hidden">Loading...</span>
        </div>
      )}
      <button className="btn btn-primary mb-3" onClick={addUser}>
        Add a user
      </button>
      <ul className="list-group">
        {users.map((user) => (
          <li
            key={user.id}
            className="list-group-item d-flex justify-content-between"
          >
            {user.name}
            <div>
              <button
                className="btn btn-secondary mx-1"
                onClick={() => updateUser(user)}
              >
                Update
              </button>
              <button
                className="btn btn-outline-danger"
                onClick={() => {
                  console.log(user.id, "deleted");
                  deleteUser(user);
                }}
              >
                Delete
              </button>
            </div>
          </li>
        ))}
      </ul>
    </>
  );
}

export default App;
				
			

Leave a Reply