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 EditCustomer 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={<EditCustomer />} /> </Routes> ) } export default Appmy-app\src\main.jsx
//src\main.jsx import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ReactQueryDevtools } from '@tanstack/react-query-devtools' //npm i @tanstack/react-query-devtools npmjs com/package/@tanstack/react-query-devtools 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 /> <ReactQueryDevtools initialIsOpen={false} /> </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 tanstack com/query/latest/docs/framework/react/installation import { getCustomerList } from "../api/customer"; import Pagination from "../components/Pagination"; import { useSearchParams } from "react-router-dom" import CustomerList from "../pages/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) }); //console.log(data); if (isLoading) return "Loading..."; if (isError) return `Error: ${error.message}`; 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 Python Django 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, useQuery, 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 CreateRead 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), }); 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 customer</button> <h1>{customer.getcustomer.name}</h1> <p>{customer.getcustomer.email}</p> </div> </div> ) } export default ReadRead 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 updateUserMutation = useMutation({ mutationFn: updateCustomer, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['customers']}); navigate("/") } }) if (isLoading) return "loading..."; if (isError) return `Error: ${error.message}`; const handleSubmit = (updatedCustomer) => { updateUserMutation.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.getcustomer.name}</h1> <UserForm onSubmit={handleSubmit} initialValue={customer.getcustomer} /> </div> </div> ) } export default EditCustomerForm : 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-gray-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 UserFormApi : my-app\src\api\customer.jsx
//src\api\customer.jsx export async function getCustomerList(page) { const response = await fetch(`http://localhost:8000/api/customers/?page=${page}`); //const response = await fetch("http://localhost:8000/api/customers/"); const data = await response.json(); //console.log(data); return { customerlist: data.results, //customerlist: data, totalpage: data.count, perpage: 3 } } export async function fetchCustomer(id) { const response = await fetch(`http://localhost:8000/api/customers/${id}/`); const data = await response.json(); //console.log(data); return { getcustomer: data } } export async function createCustomer(newCustomer) { const response = await fetch("http://localhost:8000/api/customers/", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(newCustomer) }); console.log(response); return response.json() } export async function updateCustomer(updatedCustomer) { const response = await fetch(`http://localhost:8000/api/customers/${updatedCustomer.id}/`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(updatedCustomer) }); console.log(response) return response.json() } export async function deleteCustomer(id) { const response = await fetch(`http://localhost: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
Python Django
Step 1 — Setting Up the Backend
Now install Pipenv using pip:
ednalan@Cairocoders django-nextjs % pip install pipenv
And activate a new virtual environment:
pipenv shell
ednalan@Cairocoders django-nextjs % pipenv shell
Install Django using Pipenv:
pipenv install django
ednalan@Cairocoders django-nextjs % pipenv install django
Then create a new project called backend:
django-admin startproject backend
ednalan@Cairocoders django-nextjs % django-admin startproject backend
navigate into the newly created backend directory:
cd backend
ednalan@Cairocoders backend %
Start a new application called myapp:
ednalan@Cairocoders backend % python manage.py startapp myapp
Run migrations:
ednalan@Cairocoders backend % python manage.py migrate
Navigate to http://localhost:8000 in your web browser:
Registering the myapp Application
Open the backend/settings.py file in your code editor and add myapp to the INSTALLED_APPS:
//backend/settings.py INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'myapp', ]save your changes.
Defining the Customer Model
Open the myapp/models.py file in your code editor and add the following lines of code:
//myapp/models.py from django.db import models # Create your models here. class Customer(models.Model): name = models.CharField(max_length=124) email = models.CharField(max_length=125) def _str_(self): return self.nameCreate a migration file
python manage.py makemigrations myapp
And apply the changes to the database: python manage.py migrate myapp Open the myapp/admin.py file with your code editor and add the following lines of code:
//myapp/admin.py from django.contrib import admin from .models import Customer class CustomerAdmin(admin.ModelAdmin): list_display = ('name', 'email') # Register your models here. admin.site.register(Customer, CustomerAdmin)create a “superuser” account to access the admin interface. Run the following command in your terminal:
python manage.py createsuperuser
Start the server once again:
python manage.py runserver
Navigate to http://localhost:8000/admin in your web browser. And log in with the username and password that was created You can create, edit, and, delete items using this interface:
Step 2 — Setting Up the APIs create an API using the Django REST framework.
Install the djangorestframework and django-cors-headers using Pipenv:
pipenv install djangorestframework django-cors-headers
need to add rest_framework and corsheaders to the list of installed applications. Open the backend/settings.py file in your code editor and update the INSTALLED_APPS and MIDDLEWARE sections:
INSTALLED_APPS = [
'corsheaders',
'rest_framework',
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
]
add these lines of code to the bottom of the backend/settings.py file:
CORS_ORIGIN_WHITELIST = [
'http://localhost:3000'
]
Creating serializers
serializers to convert model instances to JSON so that the frontend can work with the received data.
myapp/serializers.py file with your code editor. Open the serializers.py file and update it with the following lines of code:
//myapp/serializers.py from rest_framework import serializers from .models import Customer class CustomerSerializer(serializers.ModelSerializer): class Meta: model = Customer fields = ('id', 'name', 'email')This code specifies the model to work with and the fields to be converted to JSON.
Creating View
myapp/views.py
//myapp/views.py from django.shortcuts import render from rest_framework import viewsets from .serializers import CustomerSerializer from .models import Customer # Create your views here. class CustomerView(viewsets.ModelViewSet): serializer_class = CustomerSerializer queryset = Customer.objects.all()Open the backend/urls.py file with your code editor and replace the contents with the following lines of code:
//backend/urls.py from django.contrib import admin from django.urls import path, include from rest_framework import routers from myapp import views router = routers.DefaultRouter() router.register(r'customers', views.CustomerView, 'customer') urlpatterns = [ path('admin/', admin.site.urls), path('api/', include(router.urls)), ]restart the server:
python manage.py runserver
Navigate to http://localhost:8000/api/customers http://localhost:8000/api/customers/1 in your web browser:
Pagination
django-rest-framework
PageNumberPagination
Request:
GET https://api.example.org/accounts/?page=4
Setup
To enable the PageNumberPagination style globally, use the following configuration, and set the PAGE_SIZE as desired:
backend/settings.py
REST_FRAMEWORK = { #https://www.django-rest-framework.org/api-guide/pagination/#pagenumberpagination 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 'PAGE_SIZE': 3 }http://localhost:8000/api/customers/?page=1