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.
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:
- Wrap all the elements in one parent element like <div></div>
function ListGroup() {
return (
List
- An item
- A second item
- A third item
- A fourth item
- And a fifth one
);
}
export default ListGroup;
- 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 (
List
- An item
- A second item
- A third item
- A fourth item
- And a fifth one
);
}
export default ListGroup;
- 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 (
<>
List
- An item
- A second item
- A third item
- A fourth item
- And a fifth one
>
);
}
export default ListGroup;
- 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 (
<>
List
{items.map((item) => (
-
{item}
))}
>
);
}
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 (
... Your code inside Fragment is here.
);
}
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 (
);
}
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 (
{drink.title}
Price List:
- With Sugar: ${drink.price.withSugar}
- Without Sugar: ${drink.price.withoutSugar}
);
}
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) => (
{bug.id}:
{bug.title}
Status: {String(bug.fixed)}
));
return (
{bugsDataItems}
);
}
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 (
{bugs.map((bug) => (
{bug.title} {bug.fixed ? "Fixed" : "New"}
))}
);
}
export default App;
Sharing States between Components
NavBar.tsx
import React from "react";
interface Props {
cartItemsCount: number;
}
const NavBar = ({ cartItemsCount }: Props) => {
return NavBar: {cartItemsCount};
};
export default NavBar;
Cart.tsx
import React from "react";
interface Props {
cartItems: string[];
onClear: () => void;
}
function Cart({ cartItems, onClear }: Props) {
return (
<>
Cart
{cartItems.map((item) => (
- {item}
))}
>
);
}
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 (
setCartItems([])}>
);
}
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 {children}
;
const text = isExpanded ? children : children.substring(0, maxChars);
return (
{text}...
);
};
export default ExpandableText;
App.tsx
import ExpandableText from "./components/ExpandableText";
function App() {
return (
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?
);
}
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 (
);
};
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 (
);
};
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();
const onSubmit = (data: FieldValues) => console.log(data);
return (
);
};
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;
const Form = () => {
const {
register,
handleSubmit,
formState: { errors },
} = useForm({ resolver: zodResolver(schema) });
const onSubmit = (data: FieldValues) => console.log(data);
return (
);
};
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;
const Form = () => {
const {
register,
handleSubmit,
formState: { errors, isValid },
} = useForm({ resolver: zodResolver(schema) });
const onSubmit = (data: FieldValues) => console.log(data);
return (
);
};
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 (
Description
Amount
Category
Actions
{expenses.map((expense) => (
{expense.description}
{expense.amount}
{expense.category}
))}
Total
{/* 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. */}
$
{expenses
.reduce((acc, expense) => expense.amount + acc, 0)
.toFixed(2)}
);
};
export default ExpenseList;
ExpenseFilter.tsx
import categories from "../categories";
interface Props {
onSelectCategories: (category: string) => void;
}
const ExpenseFilter = ({ onSelectCategories }: Props) => {
return (
);
};
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;
interface Props {
onSubmit: (data: ExpenseFormData) => void;
}
const ExpenseForm = ({ onSubmit }: Props) => {
const {
register,
handleSubmit,
reset,
formState: { errors },
} = useForm({ resolver: zodResolver(schema) });
return (
);
};
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 (
setExpenses([...expenses, { ...expense, id: expenses.length + 1 }])
}
/>
setSelectedCategory(category)}
/>
setExpenses(expenses.filter((e) => e.id !== id))}
/>
);
}
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(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 (
);
};
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(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 (
);
};
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 (
<>
>
);
}
export default App;
ProductList.tsx
import React, { useEffect, useState } from "react";
const ProductList = ({ category }: { category: string }) => {
const [products, setProducts] = useState([]);
useEffect(() => {
category
? console.log("Fetching products in", category)
: console.log("Fetching List");
setProducts(["Clothings", "Accessories"]);
}, [category]);
return ProductList;
};
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 ;
}
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([]);
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("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 (
{users.map((user) => (
- {user.name}
))}
);
}
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([]);
const [error, setError] = useState("");
useEffect(() => {
// get -> promise -> res / err
axios
.get("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 {error}
;
}
return (
{users.map((user) => (
- {user.name}
))}
);
}
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([]);
const [error, setError] = useState("");
useEffect(() => {
const fetchUsers = async () => {
try {
const res = await axios.get(
"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 {error}
;
}
return (
{users.map((user) => (
- {user.name}
))}
);
}
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([]);
const [error, setError] = useState("");
useEffect(() => {
const controller = new AbortController();
axios
.get("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 (
<>
{error}
{users.map((user) => (
- {user.name}
))}
>
);
}
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([]);
const [error, setError] = useState("");
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
const controller = new AbortController();
setIsLoading(true);
axios
.get("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 (
<>
{error}
{isLoading && }
{users.map((user) => (
- {user.name}
))}
>
);
}
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([]);
const [error, setError] = useState("");
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
const controller = new AbortController();
setIsLoading(true);
axios
.get("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 (
<>
{error}
{isLoading && (
Loading...
)}
{users.map((user) => (
-
{user.name}
))}
>
);
}
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([]);
const [error, setError] = useState("");
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
const controller = new AbortController();
setIsLoading(true);
axios
.get("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 (
<>
{error}
{isLoading && (
Loading...
)}
{users.map((user) => (
-
{user.name}
))}
>
);
}
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([]);
const [error, setError] = useState("");
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
const controller = new AbortController();
setIsLoading(true);
axios
.get("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 (
<>
{error}
{isLoading && (
Loading...
)}
{users.map((user) => (
-
{user.name}
))}
>
);
}
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([]);
const [error, setError] = useState("");
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
const controller = new AbortController();
setIsLoading(true);
apiClients
.get("/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 (
<>
{error}
{isLoading && (
Loading...
)}
{users.map((user) => (
-
{user.name}
))}
>
);
}
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([]);
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("/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... 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 (
<>
{error}
{isLoading && (
Loading...
)}
{users.map((user) => (
-
{user.name}
))}
>
);
}
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("/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 or getAll
// Two approches for defining the generic endpoint:
// 1. To add the endpoint as a parameter in the paranthesis: getAll(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() {
const controller = new AbortController();
const request = apiClients
// We replace "/users" with this.endpoint
.get(this.endpoint, {
signal: controller.signal,
})
return { request, cancel: () => controller.abort() }
}
delete(id: number) {
return apiClients.delete(this.endpoint + "/" + id);
}
create(entity: T) {
return apiClients.post(this.endpoint, entity);
}
//
update(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([]);
const [error, setError] = useState("");
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
setIsLoading(true);
const { request, cancel } = userService.getAll();
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 (
<>
{error}
{isLoading && (
Loading...
)}
{users.map((user) => (
-
{user.name}
))}
>
);
}
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([]);
const [error, setError] = useState("");
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
setIsLoading(true);
const { request, cancel } = userService.getAll();
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 (
<>
{error}
{isLoading && (
Loading...
)}
{users.map((user) => (
-
{user.name}
))}
>
);
}
export default App;