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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | //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 App |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | //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>, ); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | //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> ); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | //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; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | //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 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | //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 Read |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | //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 EditCustomer |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | //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 UserForm |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | //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) { 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() } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | //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; |
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:
1 2 3 4 5 6 7 8 9 10 | //backend/settings.py INSTALLED_APPS = [ 'django.contrib.admin' , 'django.contrib.auth' , 'django.contrib.contenttypes' , 'django.contrib.sessions' , 'django.contrib.messages' , 'django.contrib.staticfiles' , 'myapp' , ] |
Defining the Customer Model
Open the myapp/models.py file in your code editor and add the following lines of code:
1 2 3 4 5 6 7 8 9 10 | //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.name |
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:
1 2 3 4 5 6 7 8 9 10 | //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) |
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:
1 2 3 4 5 6 7 8 | //myapp/serializers.py from rest_framework import serializers from .models import Customer class CustomerSerializer(serializers.ModelSerializer): class Meta: model = Customer fields = ( 'id' , 'name' , 'email' ) |
Creating View
myapp/views.py
1 2 3 4 5 6 7 8 9 10 11 | //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() |
1 2 3 4 5 6 7 8 9 10 11 12 13 | //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)), ] |
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
1 2 3 4 | REST_FRAMEWORK = { #https: //www.django-rest-framework.org/api-guide/pagination/#pagenumberpagination 'DEFAULT_PAGINATION_CLASS' : 'rest_framework.pagination.PageNumberPagination' , 'PAGE_SIZE' : 3 } |