article

Thursday, March 13, 2025

Reactjs 18 Laravel Rest API CRUD (Create, Read, Update and Delete) with Pagination | Tanstack Query Tailwind CSS

Reactjs 18 Laravel Rest API CRUD (Create, Read, Update and Delete) with Pagination | Tanstack Query Tailwind CSS

react.dev
Create react project
Run the following command in your terminal: vite dev/guide/
npm create vite@latest
tanstack/react-query
Hooks for fetching, caching and updating asynchronous data in React, Solid, Svelte and Vue

npm i @tanstack/react-query

react-query-devtools
npm i @tanstack/react-query-devtools

Tailwind CSS with Vite
tailwindcss com/docs/guides/vite

Install npm i uuid

my-app\src\App.jsx
//src\App.jsx
import { Route, Routes } from "react-router-dom" //npm i react-router-dom
import Home from "./pages/Home"
import Create from "./pages/Create"
import Read from "./pages/Read"
import Customer from "./pages/EditCustomer"

function App() { 
  return (
    <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/create/" element={<Create />} />
        <Route path="/read/:id" element={<Read />} />
        <Route path="/edit/:id" element={<Customer />} />
    </Routes>
  )
}
  
export default App
my-app\src\main.jsx
//src\main.jsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import { BrowserRouter } from "react-router-dom";
import App from './App.jsx'
 
// create a client
const queryClient = new QueryClient();
 
createRoot(document.getElementById("root")).render(
  <StrictMode>
    <BrowserRouter>
      <QueryClientProvider client={queryClient}>
        <App />
      </QueryClientProvider>
    </BrowserRouter>
  </StrictMode>,
);
Home Page : my-app\src\pages\Home.jsx
//src\pages\Home.jsx
import { useQuery } from "@tanstack/react-query"; //npm i @tanstack/react-query
import { getCustomerList } from "../api/customer";
import Pagination from "../components/Pagination";
import { useSearchParams } from "react-router-dom" 
import CustomerList from "./CustomerList";

export default function Home() {
    const [searchParams] = useSearchParams();
    const page = Number(searchParams.get("page"));
    //console.log(page)
 
    const currentPage = Number(page) || 1;
    //const currentPage = 1;

    const { isLoading, data, isError,  error } = useQuery({
        queryKey: ["customers", currentPage],
        queryFn: () => getCustomerList(currentPage)
    });

    if (isLoading) return "Loading...";
    if (isError) return `Error: ${error.message}`;
    //console.log(data);

    const totalPages = Math.ceil(Number(data.totalpage) / Number(data.perpage));
    console.log(totalPages);

  return (
    <div className="w-screen py-20 flex justify-center flex-col items-center">
        <div className="flex items-center justify-between gap-1 mb-5 pl-10 pr-10">
            <h1 className="text-4xl font-bold">Reactjs 18 Laravel Rest API CRUD (Create, Read, Update and Delete) with Pagination | Tanstack Query Tailwind CSS</h1>
        </div> 
        <div className="overflow-x-auto pt-10">
            <div className="mb-2 w-full text-right">
                <a
                href="/create"
                className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">
                Add New
                </a>
            </div>
            <CustomerList customerlist={data.customerlist} />
            <div className="flex items-center justify-between my-5">
                <Pagination totalPages={totalPages}/>
            </div>
        </div>
    </div>
  );
}
Customer List page : my-app\src\pages\CustomerList.jsx
//my-app\src\pages\CustomerList.jsx
import { useMutation, useQueryClient } from "@tanstack/react-query"; //npm i @tanstack/react-query 
import { deleteCustomer } from "../api/customer";

const CustomerList = ({ customerlist }) => { 
    const queryClient = useQueryClient();

    const deleteCustomerMutation = useMutation({
      mutationFn: deleteCustomer,
      onSuccess: () => {
        queryClient.invalidateQueries({ queryKey: ['customers']});
      }
    });
 
    const handleDelete = (id) => {
      deleteCustomerMutation.mutate(id)
    }

    return (
    <>
        <table className="table table-zebra">
            <thead className="text-sm text-gray-700 uppercase bg-gray-50">
                <tr>
                    <th className="py-3 px-6">ID</th>
                    <th className="py-3 px-6">Name</th>
                    <th className="py-3 px-6">Email</th>
                    <th className="py-3 px-6 text-center">Actions</th>
                </tr>
            </thead>
            <tbody>
                {customerlist.map((item) => (
                    <tr key={item.id} className="bg-white border-b text-black">
                        <td className="py-3 px-6">
                        {item.id}
                        </td>
                        <td className="py-3 px-6">{item.name}</td>    
                        <td className="py-3 px-6">{item.email}</td>
                        <td className="flex justify-center gap-1 py-3">
                            <a className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
                                href={`/read/${item.id}`}>Read</a> 
                              
                            <a className="focus:outline-none text-white bg-yellow-400 hover:bg-yellow-500 focus:ring-4 focus:ring-yellow-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:focus:ring-yellow-900"
                                href={`/edit/${item.id}/`}>
                                Edit
                            </a>
                            <button onClick={() => handleDelete(item.id)} className="focus:outline-none text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-900">
                                Delete</button>
                        </td>
                    </tr>
                ))}
            </tbody>
        </table>
    </>
  );
};
 
export default CustomerList;
Create page : my-app\src\pages\Create.jsx
//my-app\src\pages\Create.jsx
import { useMutation, useQueryClient } from "@tanstack/react-query"
import { createCustomer } from "../api/customer"
import UserForm from "../components/UserForm"
import {useNavigate} from 'react-router-dom'

const Create = () => {
  const queryClient = useQueryClient();
 
  const navigate = useNavigate()

  const createCustomerMutation = useMutation({
    mutationFn: createCustomer,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['customers']});
      console.log("success!");
      navigate('/')
    }
  });
 
  const handleAddPost = (customer) => {
    createCustomerMutation.mutate({
      ...customer
    })
  }
 
  return (
    <div className="w-screen py-20 flex justify-center flex-col items-center">
        <div className="max-w-md mx-auto mt-5">
        <h1 className="text-2xl text-center mb-2">Add New Customer</h1>
        <UserForm onSubmit={handleAddPost} initialValue={{}} />
        </div>
    </div>
  )
}
 
export default Create
Read page : my-app\src\pages\Read.jsx
//my-app\src\pages\Read.jsx
import { useQuery } from "@tanstack/react-query"; //npm i @tanstack/react-query
import { useNavigate, useParams } from "react-router-dom";
import { fetchCustomer } from "../api/customer";
 
const Read = () => {
    const navigate = useNavigate();
    const { id } = useParams();
    //console.log(id);

    const {
        isLoading,
        isError,
        data: customer,
        error,
    } = useQuery({
        queryKey: ["customers", id],
        queryFn: () => fetchCustomer(id),
    });
    console.log(customer);
    if (isLoading) return "loading...";
    if (isError) return `Error: ${error.message}`;
 
  return (
    <div className="w-screen py-20 flex justify-center flex-col items-center">
        <div className="overflow-x-auto py-10">
        <button className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
            onClick={() => navigate("/")}>back to list customers</button>
        <h1>{customer.results.name}</h1>
        <p>{customer.results.email}</p>
        </div>
    </div>
  )
}
 
export default Read
Read page : my-app\src\pages\EditCustomer.jsx
//my-app\src\pages\EditCustomer.jsx
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useNavigate, useParams } from "react-router-dom";
import { fetchCustomer, updateCustomer } from "../api/customer";
import UserForm from "../components/UserForm"
 
const EditCustomer = () => {
    const queryClient = useQueryClient();
    const navigate = useNavigate();
    const { id } = useParams();

    const {
        isLoading,
        isError,
        data: customer,
        error,
    } = useQuery({
        queryKey: ["customers", id],
        queryFn: () => fetchCustomer(id),
    });
    //console.log(customer);

    const updateCustomerMutation = useMutation({
        mutationFn: updateCustomer,
        onSuccess: () => {
        queryClient.invalidateQueries({ queryKey: ['customers']});
        navigate("/")
        }
    })
    
    if (isLoading) return "loading...";
    if (isError) return `Error: ${error.message}`;
    
    const handleSubmit = (updatedCustomer) => {
        updateCustomerMutation.mutate({id, ...updatedCustomer})
    }
    
 
  return (
    <div className="w-screen py-20 flex justify-center flex-col items-center">
        <div className="overflow-x-auto py-10">
            <h1>{customer.results.name}</h1>
            <UserForm onSubmit={handleSubmit} initialValue={customer.results} />
        </div>
    </div>
  )
}
 
export default EditCustomer
Form : my-app\src\components\UserForm.jsx
//src\components\UserForm.jsx
import { useState } from "react"
 
const UserForm = ({ onSubmit, initialValue }) => {
  const [user, setUser] = useState({
    name: initialValue.name || "",
    email: initialValue.email || ""
  });
 
  const handleChangeInput = (e) => {
    setUser({
      ...user,
      [e.target.name]: e.target.value
    })
  }
 
  const renderField = (label) => (
    <div>
      <label className="block text-sm font-medium text-white-900">{label}</label>
      <input onChange={handleChangeInput} type="text" name={label.toLowerCase()} value={user[label.toLowerCase()]} 
        className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500blue-500"
      />
    </div>
  );
 
  const handleSubmit = (e) => {
    e.preventDefault();
    onSubmit(user);
    setUser({
      name: "",
      email: ""
    })
 
  }
 
  return (
    <form onSubmit={handleSubmit}>
      <div className="mb-5">{renderField('Name')}</div>
      <div className="mb-5">{renderField('Email')}</div>
      <button type="submit"
        className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm w-full sm:w-auto px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
      >
        Submit</button>
    </form>
  )
}
 
export default UserForm
Api : my-app\src\api\customer.jsx
//src\api\customer.jsx
export async function getCustomerList(page) {
  const response = await fetch(`http://127.0.0.1:8000/api/customers?page=${page}`);
  const data = await response.json();
  console.log(data);
  return {
    customerlist: data.results.data,
    totalpage: data.results.total, 
    perpage: data.results.per_page
  }
}

export async function createCustomer(newCustomer) {
  const response = await fetch(`http://127.0.0.1:8000/api/customerssave`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify(newCustomer)
  });
  return response.json()
}

export async function fetchCustomer(id) {
  const response = await fetch(`http://127.0.0.1:8000/api/customers/${id}`);
  return response.json()
}

export async function updateCustomer(updatedCustomer) {
  const response = await fetch(`http://127.0.0.1:8000/api/customersupdate/${updatedCustomer.id}`, {
    method: "PUT",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify(updatedCustomer)
  });
  return response.json()
}

export async function deleteCustomer(id) {
  const response = await fetch(`http://127.0.0.1:8000/api/customers/${id}`, {
    method: "DELETE",
  });
  return response.json()
}
Pagination : my-app\src\components\Pagination.jsx
//src\components\Pagination.jsx
import { HiChevronLeft, HiChevronRight } from "react-icons/hi"; //npm install react-icons --save npmjs com/package/react-icons
import clsx from "clsx"; //npm i clsx npmjs com/package/clsx
import { useSearchParams, useLocation } from "react-router-dom" 

export const generatePagination = (currentPage, totalPages) => {
  if (totalPages <= 7) {
    return Array.from({ length: totalPages }, (_, i) => i + 1);
  }
   
  if (currentPage <= 3) {
    return [1, 2, 3, "...", totalPages - 1, totalPages];
  }
  
  if (currentPage >= totalPages - 2) {
    return [1, 2, 3, "...", totalPages - 2, totalPages - 1, totalPages];
  }
  
  return [
    1,
    "...",
    currentPage - 1,
    currentPage,
    currentPage + 1,
    "...",
    totalPages,
  ];
};

const Paginationnumber = ({ totalPages }) => {
  let location = useLocation(); 
  console.log(location);
  const { hash, pathname, search } = location;
  //console.log(pathname);

  const [searchParams] = useSearchParams();
  const currentPage = Number(searchParams.get("page")) || 1;
  //console.log(currentPage); 2

  const allPages = generatePagination(currentPage, totalPages);

  const createPageURL = (pageNumber) => {
      const params = searchParams;
      params.set("page", pageNumber.toString());
      return `${pathname}?${params.toString()}`;
  };

  const PaginationNumber = ({
      page,
      href,
      position,
      isActive,
  }) => {
      const className = clsx(
          "flex h-10 w-10 items-center justify-center text-sm border",
          {
              "rounded-l-sm": position === "first" || position === "single",
              "rounded-r-sm": position === "last" || position === "single",
              "z-10 bg-blue-700 border-blue-500 text-white bg-blue-700": isActive,
              "hover:bg-blue-700": !isActive && position !== "middle",
              "text-gray-300 pointer-events-none": position === "middle",
          }
      );

      return isActive && position === "middle" ? (
          <div className={className}>{page}</div>
      ) : (
          <a href={href} className={className}>
              {page}
          </a>
      );
  };

  const PaginationArrow = ({ href, direction, isDisabled }) => {
      const className = clsx(
          "flex h-10 w-10 items-center justify-center text-sm border",
          {
              "pointer-events-none text-blue-300": isDisabled,
              "hover:bg-blue-700": !isDisabled,
              "mr-2": direction === "left",
              "ml-2": direction === "right",
          }
      );

      const icon =
        direction === "left" ? (
          <HiChevronLeft size={20} />
        ) : (
          <HiChevronRight size={20} />
        );

    return isDisabled ? (
      <div className={className}>{icon}</div>
    ) : (
      <a href={href} className={className}>
        {icon}
      </a>
    );
  };

  return (
    <div className="inline-flex">
      <PaginationArrow
        direction="left"
        href={createPageURL(currentPage - 1)}
        isDisabled={currentPage <= 1}
      />
      <div className="flex -space-x-px">
        {allPages.map((page, index) => {
          let position;

          if (index === 0) position = "first";
          if (index === allPages.length - 1) position = "last";
          if (allPages.length === 1) position = "single";
          if (page === "...") position = "middle";

          return (
            <PaginationNumber
              key={index}
              href={createPageURL(page)}
              page={page}
              position={position}
              isActive={currentPage === page} 
            />
          );
        })}
      </div>
      <PaginationArrow
        direction="right"
        href={createPageURL(currentPage + 1)}
        isDisabled={currentPage >= totalPages}
      />
    </div>
  );
};

export default Paginationnumber;
Run
C:\react-js\my-app> npm run dev

Download Laravel App

https://laravel.com/docs/11.x/installation

Connecting our Database

open .env file root directory.

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=8889
DB_DATABASE=laravel11dev
DB_USERNAME=root
DB_PASSWORD=root

Create Model and Migration

C:\xampp\htdocs\laravel\laravelproject>php artisan make:model Customer -m

A new file named Product.php will be created in the app directory and database/migrations directory to generate the table in our database
app/Models/Customer.php
//app/Models/Customer.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Customer extends Model
{
    protected $fillable = [
        'name',
        'email',
    ];
}
database\migrations\create_customers_table.php
//database\migrations\create_customers_table.ph
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('customers', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('customers');
    }
};
Database Migration
php artisan migrate

C:\xampp\htdocs\laravel\laravel10project>php artisan migrate
Migration table created successfully.
check database table
Create Controller
C:\xampp\htdocs\laravel\laravel10project>php artisan make:controller CustomerController

app\Http\Controllers\CustomerController.php
//app\Http\Controllers\CustomerController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Customer;
use Illuminate\Support\Facades\Validator;

class CustomerController extends Controller
{
    public function index()
    {
        //$customer = Customer::all(); // All Product
        $customer = Customer::paginate(3);

        // Return Json Response
        return response()->json([
            'results' => $customer
        ], 200);
    }

    public function show($id)
    {
        $customer = Customer::find($id);
        if (!$customer) {
            return response()->json([
                'message' => 'Product Not Found.'
            ], 404);
        }

        // Return Json Response
        return response()->json([
            'results' => $customer
        ], 200);
    }

    public function save(Request $request)
    {

        $validator = Validator::make($request->all(), [
            'name' => 'required|string|max:255',
            'email' => 'required|string|email|unique:customers|max:255',
        ]);

        if ($validator->fails()) {
            return response()->json([
                'status' => false,
                'message' => 'Validation error',
                'errors' => $validator->errors()
            ], 422);
        }

        $customer = Customer::create([
            'name' => $request->name,
            'email' => $request->email
        ]);

        return response()->json([
            'status' => true,
            'message' => 'Customer created successfully',
            'data' => $customer
        ], 201);
    }

    public function update(Request $request, $id)
    {
        $validator = Validator::make($request->all(), [
            'name' => 'required|string|max:255',
            'email' => 'required|string|email|max:255|unique:customers,email,' . $id,
        ]);

        if ($validator->fails()) {
            return response()->json([
                'status' => false,
                'message' => 'Validation error',
                'errors' => $validator->errors()
            ], 422);
        }

        $customer = Customer::findOrFail($id);
        $customer->update($request->all());
        
        return response()->json([
            'status' => true,
            'message' => 'Customer updated successfully',
            'data' => $customer
        ], 200);
    }

    public function destroy($id)
    {
        $customer = Customer::findOrFail($id);
        $customer->delete();

        return response()->json([
            'status' => true,
            'message' => 'Customer deleted successfully'
        ], 204);
    }
}
Routes
install
php artisan install:api
All API requests will need the header Accept: application/json.
open routes/api.php and update the following code
routes\api.php
//routes\api.php
<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\CustomerController;

Route::get('/user', function (Request $request) {
    return $request->user();
})->middleware('auth:sanctum');


Route::get('/customers', [CustomerController::class, 'index']);
Route::get('/customers/{id}', [CustomerController::class, 'show']);
Route::post('/customers', [CustomerController::class, 'store']);
Route::post('/customerssave', [CustomerController::class, 'save']);
Route::put('/customersupdate/{id}', [CustomerController::class, 'update']);
Route::delete('/customers/{id}', [CustomerController::class, 'destroy']);
Run C:\xampp\htdocs\laravel\myapp>php artisan serve
Starting Laravel development server: http://127.0.0.1:8000

Related Post