Go Back to Home
image featured post
React JS

Dashboard Pizza Shop using React.js. Part 4 – API Connection

  • Rui Vergani Neto
  • 29, August, 2024
  • 11 min read
Search through topics

GiHub: https://github.com/ruivergani/dashboard-shop-web

Updating Profile

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.

Code breakdown:

// Update Profile function
const { mutateAsync: updateProfileFn } = useMutation({
  mutationFn: updateProfile,
});

1. useMutation Hook:

  • useMutation is a hook provided by React Query specifically for handling mutations (operations that modify data on the server).
  • Mutations are typically associated with POST, PUT, PATCH, or DELETE requests.

2. Destructuring Assignment:

  • const { mutateAsync: updateProfileFn } is using JavaScript’s destructuring to extract the mutateAsync function from the object returned by useMutation.
  • The mutateAsync function is then renamed to updateProfileFn for easier usage in your code.

3. mutationFn:

  • mutationFn: updateProfile specifies the function to be called when the mutation is triggered.
  • updateProfile is a function that you’ve likely defined elsewhere in your code. This function would handle the actual API call to update the profile on the server.
  • This function should return a promise that resolves once the server has processed the update.
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:

  • This is an asynchronous function that takes data as an argument. The data parameter is expected to follow the structure defined by StoreProfileSchema, which likely includes fields like name and description.

2. Calling updateProfileFn:

  • Inside handleUpdateProfile, the updateProfileFn (which was destructured from useMutation) is called with an object containing a name and description.
  • updateProfileFn triggers the mutation, i.e., it sends the request to update the profile on the server.

3. try…catch Block:

  • The try block is used to handle the asynchronous operation safely. If the mutation is successful, the next line of code will run.
  • If the mutation fails (e.g., due to network issues or server errors), control jumps to the catch block.

4. Showing Success or Error Toasts:

  • If the mutation is successful, toast.success(“Profile updated successfully.”); displays a success message to the user.
  • If an error occurs during the mutation, the catch the block displays an error message using toast.error(“Error when updating profile, try again.”);.
// 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:

  • useQuery Hook: Fetches and manages the state of data, in this case, related to a “managed restaurant.”
  • queryKey: [“managed-restaurant”]: A unique key that identifies this query, allowing React Query to cache and manage the data.
  • queryFn: getManagedRestaurant: The function that actually fetches the “managed restaurant” data.
  • staleTime: 1000: Sets the data to be considered “fresh” for 1000 milliseconds (1 second). After this time, React Query treats the data as “stale” and may refetch it under certain conditions.
  • data: managedRestaurant: Destructures the fetched data and renames it to managedRestaurant.
  • isLoading: isLoadingManagedRestaurant: Destructures the loading state and renames it to isLoadingManagedRestaurant, indicating if the data is still being fetched.

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.

Updating HTTP state

In this lesson, we will learn about the different types of state in React applications: Local State, HTTP State, and Global State.

  • Local State is the state that we place inside components using useState.
  • Global State is when we use libraries like Redux or Zustand to have global states accessible by multiple components.
  • HTTP State is the state of the data returned by HTTP requests, like GET requests made to an API.

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:

  • useMutation is used for handling mutations, such as updating, deleting, or creating data on the server.

2. Destructuring Assignment:

  • const { mutateAsync: updateProfileFn } destructures the mutateAsync function from useMutation and renames it to updateProfileFn for easier use.

3. mutationFn:

  • mutationFn: updateProfile specifies the function (updateProfile) that will be called to perform the update on the server. This function is expected to return a promise.

4. onSuccess Callback:

  • The onSuccess callback is executed after the mutation is successfully completed.
  • The first argument _ is the data returned from the mutation (not used in this case).
  • The second argument { name, description } contains the variables passed to mutateAsync when the mutation was triggered.

5. Cache Manipulation with queryClient:

  • queryClient.getQueryData([“managed-restaurant”]): Retrieves the current cached data for the “managed-restaurant” query. If the cache exists, the code proceeds to update it.
  • queryClient.setQueryData([“managed-restaurant”], {…cached, name, description}): Updates the cached data by merging the existing data (cached) with the new name and description values from the mutation. This ensures that the UI reflects the updated profile information immediately, without waiting for the next refetch.

Interface Optimistic

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):

  • Purpose: onMutate is triggered before the mutation function (updateProfile) is executed. It allows you to optimistically update the cache and manage the UI state immediately, giving a smooth user experience.
  • Parameters: {name, description} are the variables being passed to the mutation.
  • Cache Update: The updateManagedRestaurantCache function is called with the new name and description. This function updates the cache for the “managed restaurant” with the new values.
  • cached: This is the previous data before the optimistic update.
  • Return Value: The function returns an object { previousProfile: cached } that contains the previous state of the profile. This object is stored in context and can be used later in onError to revert changes if the mutation fails.

4. onError Callback (Rollback on Error):

  • Purpose: onError is triggered if the mutation fails. This is where you handle rolling back any optimistic updates to the cache.
  • Parameters: The first two parameters _ 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.
  • Rollback: If the mutation fails, the cache is reverted to the previous state using updateManagedRestaurantCache(context.previousProfile). This ensures that the UI reflects the correct data even after a failed mutation.

User Logout

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>

Listing of Orders

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>
    </>
  )
}

Order Pagination

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>
    </>
  )
}

Similar Articles

Check similiar articles below 🚀