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 App
my-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 Flask 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 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),
});
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.customer.name}</h1>
<p>{customer.getcustomer.customer.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 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.customer} />
</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-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
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:5000/api/customerpagination/${page}/3`);
//const response = await fetch("http://127.0.0.1:5000/api/customer/");
const data = await response.json();
//console.log(data);
return {
customerlist: data.customers,
totalpage: data.total,
perpage: data.per_page
}
}
export async function fetchCustomer(id) {
const response = await fetch("http://127.0.0.1:5000/api/customer/"+id);
const data = await response.json();
console.log(data);
return {
getcustomer: data
}
}
export async function createCustomer(newCustomer) {
const response = await fetch("http://127.0.0.1:5000/api/customer", {
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://127.0.0.1:5000/api/customer/${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://127.0.0.1:5000/api/customer/${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-guide-es.readthedocs.io/es/guide-es/dev/virtualenvs.html
Create an environment
ednalan@Cairocoders myapp % pip install virtualenv
ednalan@Cairocoders myapp % pip install virtualenv
Activate the environment
ednalan@Cairocoders myapp % source venv/bin/activate
(venv) ednalan@Cairocoders myapp %
Install Flask
pypi.org/project/Flask/
(venv) ednalan@Cairocoders myapp % pip install -U Flask
(venv) ednalan@Cairocoders myapp % flask run
Install requirements
pip install -U flask-cors
pypi.org/project/Flask-Cors/
Flask-SQLAlchemy
Flask-SQLAlchemy is an extension for Flask that adds support for SQLAlchemy to your application.
flask-sqlalchemy.palletsprojects.com/en/3.0.x/
(venv) PS C:\flask_dev\myapp>pip install -U Flask-SQLAlchemy
Flask + marshmallow for beautiful APIs
pypi.org/project/flask-marshmallow/
(venv) PS C:\flask_dev\myapp>pip install flask-marshmallow
python3 -m pip install
https://pypi.org/project/pymysql/
app.py
//app.py
from flask import Flask, request, jsonify, make_response
from flask_cors import CORS, cross_origin #ModuleNotFoundError: No module named 'flask_cors' = pip install Flask-Cors
from models import db, Customer
app = Flask(__name__)
app.config['SECRET_KEY'] = 'cairocoders-ednalan'
#app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///flaskdb.db'
# Databse configuration mysql Username:password@hostname/databasename
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://root:root@localhost:8889/reactjsdb'
SQLALCHEMY_TRACK_MODIFICATIONS = False
SQLALCHEMY_ECHO = True
CORS(app, supports_credentials=True)
db.init_app(app)
with app.app_context():
db.create_all()
@app.route("/")
def hello():
return "Hello, World!"
# create a test route
@app.route('/test', methods=['GET'])
def test():
return jsonify({'message': 'The server is running'})
# create a customer
@app.route('/api/customer', methods=['POST'])
def create_customer():
try:
data = request.get_json()
new_customer = Customer(name=data['name'], email=data['email'])
db.session.add(new_customer)
db.session.commit()
return jsonify({
'id': new_customer.id,
'name': new_customer.name,
'email': new_customer.email
}), 201
except Exception as e:
return make_response(jsonify({'message': 'error creating customer', 'error': str(e)}), 500)
# get all customer
@app.route('/api/customer', methods=['GET'])
def get_customer():
try:
customer = Customer.query.all()
customer_data = [{'id': customer.id, 'name': customer.name, 'email': customer.email} for customer in customer]
return jsonify(customer_data), 200
except Exception as e:
return make_response(jsonify({'message': 'error getting customer', 'error': str(e)}), 500)
@app.route('/api/customerpagination/<int:page>/<int:per_page>', methods=['GET'])
def customerpagination(page=1, per_page=3):
total = Customer.query.count()
print(total)
customers = Customer.query.order_by(Customer.id.asc())
customers = customers.paginate(page=page, per_page=per_page)
return jsonify({
'total': total,
'page': page,
'per_page': per_page,
'has_next': customers.has_next,
'has_prev': customers.has_prev,
'page_list': [iter_page if iter_page else '...' for iter_page in customers.iter_pages()],
'customers': [{
'id': p.id,
'name': p.name,
'email': p.email
} for p in customers.items]
})
# get a customers by id
@app.route('/api/customer/<id>', methods=['GET'])
def get_customerid(id):
try:
customer = Customer.query.filter_by(id=id).first() # get the first customer with the id
if customer:
return make_response(jsonify({'customer': customer.json()}), 200)
return make_response(jsonify({'message': 'customer not found'}), 404)
except Exception as e:
return make_response(jsonify({'message': 'error getting customer', 'error': str(e)}), 500)
# update a customer by id
@app.route('/api/customer/<id>', methods=['PUT'])
def update_customer(id):
try:
customer = Customer.query.filter_by(id=id).first()
if customer:
data = request.get_json()
customer.name = data['name']
customer.email = data['email']
db.session.commit()
return make_response(jsonify({'message': 'customer updated'}), 200)
return make_response(jsonify({'message': 'customer not found'}), 404)
except Exception as e:
return make_response(jsonify({'message': 'error updating customer', 'error': str(e)}), 500)
# delete a customer by id
@app.route('/api/customer/<id>', methods=['DELETE'])
def delete_customer(id):
try:
customer = Customer.query.filter_by(id=id).first()
if customer:
db.session.delete(customer)
db.session.commit()
return make_response(jsonify({'message': 'customer deleted'}), 200)
return make_response(jsonify({'message': 'customer not found'}), 404)
except Exception as e:
return make_response(jsonify({'message': 'error deleting customer', 'error': str(e)}), 500)
if __name__ == "__main__":
app.run(debug=True)
models.py
//models.py
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
class Customer(db.Model):
__tablename__ = 'customer'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
def json(self):
return {'id': self.id,'name': self.name, 'email': self.email}
run (venv) C:\flask_dev\myapp>flask run 