GiHub: https://github.com/ruivergani/dashboard-shop-web
In this lesson, we will learn how to update a user’s profile. First, we will create a function in the API to handle the profile update.
import { api } from "@/lib/axios"
interface UpdateProfileBody {
name: string
description: string | null
}
export async function updateProfile({ name, description }: UpdateProfileBody) {
await api.put("/profile", { name, description })
}
Then, we will use a mutation to perform the update on the front-end. When the form is submitted, the handleUpdateProfile function will be called, and the form data will be passed in. If the update is successful, we will display a success message. If not, we will display an error message.
We will also add a cancel button to close the modal. In the next lesson, we will see how to automatically update the information on the screen after a profile update.
// Update Profile function
const { mutateAsync: updateProfileFn } = useMutation({
mutationFn: updateProfile,
});
1. useMutation Hook:
2. Destructuring Assignment:
mutateAsync
function from the object returned by useMutation.3. mutationFn:
:
updateProfile specifies the function to be called when the mutation is triggered.async function handleUpdateProfile(data: StoreProfileSchema) {
try {
await updateProfileFn({
name: data.name,
description: data.description,
});
toast.success("Profile updated successfully.");
} catch {
toast.error("Error when updating profile, try again.");
}
}
1. handleUpdateProfile Function:
2. Calling updateProfileFn:
3. try…catch Block:
4. Showing Success or Error Toasts:
// Understanding React Query
const { data: managedRestaurant, isLoading: isLoadingManagedRestaurant } =
useQuery({
queryKey: ["managed-restaurant"], // identification for the same request in different places
queryFn: getManagedRestaurant,
staleTime: 1000,
})
This code uses the useQuery
hook from React Query to fetch data about a “managed restaurant.” Here’s a brief explanation:
Summary = The code fetches data about a managed restaurant and stores it in managedRestaurant. The loading state is tracked with isLoadingManagedRestaurant, and the data is considered fresh for 1 second.
const { data: managedRestaurant } = useQuery({
queryKey: ["managed-restaurant"], // identification for the same request in different places
queryFn: getManagedRestaurant,
staleTime: Infinity, // do not load this when return to screen focus
})
If you set staleTime to Infinity will avoid refetching the data when the screen focus returns.
In this lesson, we will learn about the different types of state in React applications: Local State, HTTP State, and Global State.
We will see how to update this data using React Query or similar libraries. We will show how to do this using the useMutation hook and the onSuccess function. We will fetch the current data using queryClient.getQueryData and update the data using queryClient.setQueryData.
We will show how to do this in practice and how we can type this data using interfaces. This is an amazing feature of React Query and is one of the most important ones that we will learn.
// store-profile-dialog.tsx
// Update Profile function
const { mutateAsync: updateProfileFn } = useMutation({
mutationFn: updateProfile,
onSuccess(_, { name, description }) {
const cached = queryClient.getQueryData(["managed-restaurant"])
if (cached) {
queryClient.setQueryData(["managed-restaurant"], {
...cached,
name,
description,
})
}
},
})
1. useMutation Hook:
2. Destructuring Assignment:
3. mutationFn
:
4. onSuccess Callback:
_
is the data returned from the mutation (not used in this case).5. Cache Manipulation with queryClient:
In this lesson, we explored cache refresh in HTTP requests. We learned about the concept of an optimistic interface, which allows you to react to changes in user input before they are even confirmed on the backend.
To implement an optimistic interface, we separate the refresh code into a separate function and call it in the onMutate event, which is triggered when the user clicks the save button.
// onMutate is triggered when the user clicks the save button
const { mutateAsync: updateProfileFn } = useMutation({
mutationFn: updateProfile,
// Cache manipulation with queryClient
onMutate({ name, description }) {
const { cached } = updateManagedRestaurantCache({ name, description })
return { previousProfile: cached }
},
onError(_, __, context) {
if (context?.previousProfile) {
updateManagedRestaurantCache(context.previousProfile)
}
},
})
We also use the onError event to handle errors in the request, reverting the cache to the original data if a problem occurs. An optimistic interface is a powerful technique for improving the user experience in situations where the probability of error is low.
3. onMutate Callback (Optimistic Update):
cached
: This is the previous data before the optimistic update.4. onError
Callback (Rollback on Error):
onError
is triggered if the mutation fails. This is where you handle rolling back any optimistic updates to the cache._
and __
are placeholders for the error and variables, which are not used in this function. The third parameter context
is the object returned from onMutate
, which contains previousProfile
.In this final part of the lesson, we will implement the logout functionality. We will create a file called sign-out.ts in the API route. In this file, we will have a simple function that will make a POST request to the /sign-out route.
Going back to the account menu, we will create a mutation called signout that will use the asynchronous mutate function to call the logout function. We will also add a variable called isSigningOut to track the logout state. When the logout function completes successfully, we will use the React Router DOM’s useNavigate to redirect the user to the login route, replacing the current route.
On the logout button, we will add the disabled property to track the logout state and use a native HTML button with an onClick event to call the logout function.
Finally, when the logout button is clicked, the cookie will be cleared and the user will be redirected to the login page. Now, we can start loading application information, such as metrics, popular products, and orders.
// sign-out.ts
import { api } from "@/lib/axios"
export async function signOut() {
await api.post("/sign-out")
}
// account-menu.tsx
// Sign Out Function
const { mutateAsync: signOutFn, isPending: isSigningOut } = useMutation({
mutationFn: signOut,
onSuccess: () => {
navigate("/sign-in", { replace: true }) // substituir a rota evitando usuario clicar no botao de voltar e retornar para o dashboard
},
})
<DropdownMenuItem className="text-rose-500 dark:text-rose-400" asChild disabled={isSigningOut}>
<button
onClick={() => signOutFn()}
className="w-full"
>
<LogOut className="mr-2 h-4 w-4"></LogOut>
<span>Exit</span>
</button>
</DropdownMenuItem>
In this lesson, we will start creating the order listing structure, including filters and pagination. We will create an API function called get-orders.ts, which will receive parameters related to filters and pagination.
// get-orders.tsx
import { api } from "@/lib/axios"
export interface GetOrdersResponse {
orders: {
orderId: string
createdAt: string
status: "pending" | "canceled" | "processing" | "delivering" | "delivered"
customerName: string
total: number
}[]
meta: {
pageIndex: number
perPage: number
totalCount: number
}
}
export async function getOrders() {
const response = await api.get<GetOrdersResponse>("/orders", {
params: {
pageIndex: 0,
},
})
return response.data
}
We will use the useQuery function to make the API call and get the order data. Next, we will fill the order information in the table, such as the order ID, status, customer name, and price. We will also create a component called order-status.tsx to display the order status in different colors.
Finally, we will use the date-fns library to format the order creation date.
// install date fns library
pnpm i date-fns
Create a component called order-status.tsx
// order-status.tsx
type OrderStatus =
| "pending"
| "canceled"
| "processing"
| "delivering"
| "delivered"
interface OrderStatusProps {
status: OrderStatus
}
// for each state I will add text
const orderStatusMap: Record<OrderStatus, string> = {
pending: "Pending",
canceled: "Canceled",
processing: "Processing",
delivering: "Delivering",
delivered: "Delivered",
}
export function OrderStatus({ status }: OrderStatusProps) {
return (
<div className="flex items-center gap-2">
{status === "pending" && (
<span className="h-2 w-2 rounded-full bg-slate-400"></span>
)}
{status === "canceled" && (
<span className="h-2 w-2 rounded-full bg-red-600"></span>
)}
{status === "delivered" && (
<span className="h-2 w-2 rounded-full bg-green-600"></span>
)}
{["processing", "delivering"].includes(status) && (
<span className="h-2 w-2 rounded-full bg-amber-500"></span>
)}
<span className="font-medium text-muted-foreground">
{orderStatusMap[status]}
</span>
</div>
)
}
In the order-table-row.tsx component we have added the props, configured the date using date-fns library, and also implemented the order-status.tsx component.
// order-table-row.tsx
import { formatDistanceToNow } from "date-fns"
import { ArrowRight, Search, X } from "lucide-react"
import { OrderStatus } from "@/components/order-status"
import { Button } from "@/components/ui/button"
import { Dialog, DialogTrigger } from "@/components/ui/dialog"
import { TableCell, TableRow } from "@/components/ui/table"
import { OrderDetails } from "./order-details"
export interface OrderTableRowProps {
order: {
orderId: string
createdAt: string
status: "pending" | "canceled" | "processing" | "delivering" | "delivered"
customerName: string
total: number
}
}
export function OrderTableRowCustom({ order }: OrderTableRowProps) {
return (
<TableRow>
<TableCell>
<Dialog>
<DialogTrigger asChild>
<Button
variant="outline"
size="xs"
>
<Search className="h-3 w-3"></Search>
<span className="sr-only">Details of order</span>
</Button>
</DialogTrigger>
<OrderDetails />
</Dialog>
</TableCell>
<TableCell className="font-mono text-xs font-medium">
{order.orderId}
</TableCell>
<TableCell className="text-muted-foreground">
{formatDistanceToNow(order.createdAt, {
addSuffix: true, // add the word ago
})}
</TableCell>
<TableCell>
<OrderStatus status={order.status}></OrderStatus>
</TableCell>
<TableCell className="font-medium">{order.customerName}</TableCell>
<TableCell className="font-medium">
{order.total.toLocaleString("gb-EN", {
style: "currency",
currency: "GBP",
})}
</TableCell>
<TableCell>
<Button
variant="outline"
size="xs"
>
<ArrowRight className="mr-2 h-3 w-3" />
Approve
</Button>
</TableCell>
<TableCell>
<Button
variant="ghost"
size="xs"
>
<X className="mr-2 h-3 w-3"></X>
Cancel
</Button>
</TableCell>
</TableRow>
)
}
This is how the orders.tsx file will look like:
// orders.tsx
import { useQuery } from "@tanstack/react-query"
import { Helmet } from "react-helmet-async"
import { getOrders } from "@/api/get-orders"
import { Pagination } from "@/components/pagination"
import {
Table,
TableBody,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { OrderTableFilter } from "./order-table-filters"
import { OrderTableRowCustom } from "./order-table-row"
export function Orders() {
// Order Query
const { data: result } = useQuery({
queryKey: ["orders"],
queryFn: getOrders,
})
return (
<>
<Helmet title="Orders" />
<div className="flex flex-col gap-4">
<h1 className="text-3xl font-bold tracking-tight">Orders</h1>
<div className="space-y-2.5">
<OrderTableFilter></OrderTableFilter>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[64px]"></TableHead>
<TableHead className="w-[140px]">Identificator</TableHead>
<TableHead className="w-[180px]">Carried out</TableHead>
<TableHead className="w-[140px]">Status</TableHead>
<TableHead>Client</TableHead>
<TableHead className="w-[140px]">Total of Order</TableHead>
<TableHead className="w-[164px]"></TableHead>
<TableHead className="w-[132px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{result &&
result.orders.map(order => {
return (
<OrderTableRowCustom
key={order.orderId}
order={order}
></OrderTableRowCustom>
)
})}
</TableBody>
</Table>
</div>
<Pagination
pageIndex={0}
totalCount={105}
perPage={10}
/>
</div>
</div>
</>
)
}
In this lesson, we will work on implementing order pagination.
We will start by modifying the getOrders function to receive a pageIndex parameter, which represents the current page.
// get-orders.ts
import { api } from "@/lib/axios"
export interface GetOrdersQuery{
pageIndex?: number | null
}
export interface GetOrdersResponse {
orders: {
orderId: string
createdAt: string
status: "pending" | "canceled" | "processing" | "delivering" | "delivered"
customerName: string
total: number
}[]
meta: {
pageIndex: number
perPage: number
totalCount: number
}
}
// Function connecting to the API (the parameter now receives the pageIndex)
export async function getOrders({ pageIndex }: GetOrdersQuery) {
const response = await api.get<GetOrdersResponse>("/orders", {
params: {
pageIndex,
},
})
return response.data
}
Then, we will use the react-router-dom useSearchParams hook to save the page state in the URL. This way, the state will be maintained even if the page is reloaded.
// this is the simple logic behind
// Pagination Function
const [searchParams, setSearchParams] = useSearchParams(); // save state in the URL
const pageIndex = searchParams.get('page') ?? 0;
In addition, we will make some improvements to the pagination interface, such as disabling the navigation buttons when it is not possible to go back or forward to another page. We will also update the queryKey of the query function so that the request is made whenever the pageIndex parameter is changed.
Note:
This code gets the “page” parameter from the URL, converts it to a number, and subtracts 1 to align it with a 0-based index system. If no “page” is provided, it defaults to 0.
// explanation for this part of the code
// This code gets the "page" parameter from the URL, converts it to a number, and subtracts 1 to align it with a 0-based index system. If no "page" is provided, it defaults to 0.
const pageIndex = z.coerce
.number()
.transform(page => page - 1)
.parse(searchParams.get("page") ?? "0")
In the end, we will have a functional pagination component, which allows the user to navigate between order pages in an intuitive way.
// pagination.tsx
import {
ChevronLeft,
ChevronRight,
ChevronsLeft,
ChevronsRight,
} from "lucide-react"
import { Button } from "@/components/ui/button"
export interface PaginationProps {
pageIndex: number
totalCount: number
perPage: number
onPageChange: (pageIndex: number) => Promise<void> | void
}
export function Pagination({
pageIndex,
totalCount,
perPage,
onPageChange,
}: PaginationProps) {
// Total pages number
const pages = Math.ceil(totalCount / perPage) || 1 // round up
// Component
return (
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">
Total of {totalCount} items(s)
</span>
<div className="flex items-center gap-6 lg:gap-8">
<div className="text-sm font-medium">
Page {pageIndex + 1} of {pages}
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
className="h-8 w-8 p-0"
onClick={() => onPageChange(0)}
disabled={pageIndex === 0}
>
<ChevronsLeft className="h-4 w-4" />
<span className="sr-only">First Page</span>
</Button>
<Button
variant="outline"
className="h-8 w-8 p-0"
onClick={() => onPageChange(pageIndex - 1)}
disabled={pageIndex === 0}
>
<ChevronLeft className="h-4 w-4" />
<span className="sr-only">Previous Page</span>
</Button>
<Button
variant="outline"
className="h-8 w-8 p-0"
onClick={() => onPageChange(pageIndex + 1)}
disabled={pages <= pageIndex + 1}
>
<ChevronRight className="h-4 w-4" />
<span className="sr-only">Next Page</span>
</Button>
<Button
variant="outline"
className="h-8 w-8 p-0"
onClick={() => onPageChange(pages - 1)}
disabled={pages <= pageIndex + 1}
>
<ChevronsRight className="h-4 w-4" />
<span className="sr-only">Last Page</span>
</Button>
</div>
</div>
</div>
)
}
Now you can modify the orders.tsx.
// orders.tsx
import { useQuery } from "@tanstack/react-query"
import { Helmet } from "react-helmet-async"
import { useSearchParams } from "react-router-dom"
import { z } from "zod"
import { getOrders } from "@/api/get-orders"
import { Pagination } from "@/components/pagination"
import {
Table,
TableBody,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { OrderTableFilter } from "./order-table-filters"
import { OrderTableRowCustom } from "./order-table-row"
export function Orders() {
// Pagination Function => bear in mind that the state of each component is not keep after reloading
const [searchParams, setSearchParams] = useSearchParams() // save state in the URL
const pageIndex = z.coerce
.number()
.transform(page => page - 1)
.parse(searchParams.get("page") ?? "1")
function handlePaginate(pageIndex: number) {
setSearchParams(state => {
state.set("page", (pageIndex + 1).toString())
return state
})
}
// Order Query
const { data: result } = useQuery({
queryKey: ["orders", pageIndex], // toda vez que depender de parametro tem que ser incluso aqui
queryFn: () => getOrders({ pageIndex }),
})
return (
<>
<Helmet title="Orders" />
<div className="flex flex-col gap-4">
<h1 className="text-3xl font-bold tracking-tight">Orders</h1>
<div className="space-y-2.5">
<OrderTableFilter></OrderTableFilter>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[64px]"></TableHead>
<TableHead className="w-[140px]">Identificator</TableHead>
<TableHead className="w-[180px]">Carried out</TableHead>
<TableHead className="w-[140px]">Status</TableHead>
<TableHead>Client</TableHead>
<TableHead className="w-[140px]">Total of Order</TableHead>
<TableHead className="w-[164px]"></TableHead>
<TableHead className="w-[132px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{result &&
result.orders.map(order => {
return (
<OrderTableRowCustom
key={order.orderId}
order={order}
></OrderTableRowCustom>
)
})}
</TableBody>
</Table>
</div>
{result && (
<Pagination
pageIndex={result.meta.pageIndex}
totalCount={result.meta.totalCount}
perPage={result.meta.perPage}
onPageChange={handlePaginate}
/>
)}
</div>
</div>
</>
)
}
Check similiar articles below 🚀