A production-ready quick start that connects the Syncfusion® React Pivot Table to a Python Flask backend using custom fetch-based binding — enabling remote data loading and full CRUD operations over REST endpoints.
- 🚀 Quick Overview
- ✨ Key Features
- 🛠️ Prerequisites
- 📂 Project Structure
- ⚙️ Installation & Setup
▶️ Running the Application- 🧪 Testing CRUD Operations
- 🔧 Troubleshooting
- 📖 API Reference
- 🤝 Contributing
- 📜 License & Support
- 📚 Related Resources
This project demonstrates how to bind the Syncfusion® React Pivot Table to a remote Python Flask backend using custom fetch-based binding with the drill-through editing grid. Unlike the FastAPI sample (which uses UrlAdaptor), this project uses a useEffect-based initial data load and an actionComplete event handler attached to the drill-through grid to dispatch POST, PUT, and DELETE requests to the Flask REST API. The drill-through grid is the place where all CRUD operations are performed — double-click any value cell in the Pivot Table to open it.
| Component | Technology | Purpose |
|---|---|---|
| 🎨 Frontend | React 19 + Vite + Syncfusion® EJ2 | Render the interactive Pivot Table UI |
| ⚙️ Backend | Python 3.11+ + Flask + Flask-CORS | Serve data, perform CRUD, return JSON responses |
| 🔌 Binding | fetch + useEffect + drill-through actionComplete | Bridge between Pivot Table and Flask REST endpoints |
| 📊 Sample Data | In-memory products list (from products_data.json) | Simulate product sales records for the Pivot Table |
💡 Flask is a lightweight Python web framework that makes it easy to build REST APIs for web applications. The backend in this project follows a simple, readable layout — one
app.pyfile containing all routes (GET,POST,PUT,DELETE) and an in-memory data store loaded fromproducts_data.jsonat startup. CRUD requests from the drill-through grid are dispatched through standard HTTP methods.
- 📊 Remote Data Binding – Connects the Pivot Table to a Flask REST endpoint over HTTP.
- 🔄 Full CRUD Support – Insert, update, and delete records directly from the Pivot Table drill-through grid.
- 🐍 Flask API Backend – Built with Flask and Flask-CORS for a minimal, readable REST API.
- 🗂️ Plain JSON Response –
GET /productsreturns the product array as plain JSON. The React client assigns it directly todataSourceSettings.dataSource. - 🔑 Primary Key Configuration – Uses
ProductIDas the primary key for unique record identification during update and delete. - 🌐 CORS-Enabled – Preconfigured with
Flask-CORSto allow cross-origin requests from the Vite dev server. - ⚡ Drill-Through Editing – Double-click a pivot cell to add, edit, or delete underlying records in a pop-up grid.
- 🛡️ Robust Error Handling – Routes return meaningful HTTP status codes (
201,404) and clear error messages. - 📦 Ready-to-Run – Clone, install, and start both projects — no database setup required (in-memory sample data).
Make sure the following software and packages are installed on your machine before running the project.
| Software / Package | Version | Purpose |
|---|---|---|
| 🐍 Python | 3.11 or later | Runtime for the Flask backend |
| 📦 venv | Included with Python | Creates an isolated Python environment for the backend |
| 🌶️ Flask | 2.0 or later | REST API framework |
| 🔀 Flask-CORS | 3.0 or later | Allows requests from the React dev server |
| 🟢 Node.js | 20.x LTS or later | Runtime for the React dev server |
| 📦 npm / yarn / pnpm | Latest stable | Package manager |
| ⚛️ React | 19.x or later | Build the Pivot Table client |
| ⚡ Vite | 7.3 or later | React dev server and build tool |
| 📦 @syncfusion/ej2-react-pivotview | 33.1.45+ | React Pivot Table component |
| 📦 @syncfusion/ej2-tailwind3-theme | Latest | Syncfusion theme stylesheet for the Tailwind 3 theme |
syncfusion-react-pivot-with-flask-api/
├── 📁 Client/ # React frontend (Pivot Table) — Vite + TypeScript
│ ├── 📁 public/
│ ├── 📁 src/
│ │ ├── App.css # Component styles
│ │ ├── App.tsx # Pivot Table with custom fetch binding + CRUD handlers
│ │ ├── index.css
│ │ ├── main.tsx # React entry point
│ │ └── 📁 assets/
│ ├── index.html
│ ├── package.json # React dependencies & scripts
│ ├── tsconfig.app.json
│ ├── tsconfig.json
│ ├── tsconfig.node.json
│ └── vite.config.ts
│
├── 📁 FlaskAPIServer/ # Python backend (Flask + Flask-CORS)
│ ├── app.py # Flask app: CORS, /products routes (GET/POST/PUT/DELETE)
│ └── products_data.json # Sample product data source (16 records)
│
├── 📄 README.md # You are here
└── 📄 flaskapi-server.md # UG documentation source for this sample
git clone https://github.com/SyncfusionExamples/syncfusion-react-pivot-with-flask-api.git
cd syncfusion-react-pivot-with-flask-apiThe backend project lives in the FlaskAPIServer/ folder.
A virtual environment keeps the Python packages used by this backend separate from other projects on your machine.
cd FlaskAPIServer
python -m venv venv
# Windows (PowerShell)
.\venv\Scripts\Activate.ps1
# macOS / Linuxsource venv/bin/activatepip install flask flask-corsPackage descriptions:
- flask – Creates the REST API and handles HTTP requests and responses.
- flask-cors – Allows the React application to communicate with the Flask backend from a different origin.
products_data.json provides the in-memory data source for the Pivot Table. It contains product records with the following fields.
| Field | Data type | Description |
|---|---|---|
ProductID | number | Unique product identifier (primary key) |
ProductName | string | Name of the product |
Category | string | Category to which the product belongs |
MRP | number | Maximum Retail Price of the product |
Discount | number | Discount value applied to the product |
The first three records are shown below for brevity. The complete file contains 16 product records (identical ProductName values across four Category values, with incrementing MRP and Discount).
[
{
"ProductID": 10001,
"ProductName": "Smartwatch",
"Category": "Electronics",
"MRP": 100.0,
"Discount": 1.02
},
{
"ProductID": 10002,
"ProductName": "Smartwatch",
"Category": "Accessories",
"MRP": 110.0,
"Discount": 1.12
},
{
"ProductID": 10003,
"ProductName": "Smartwatch",
"Category": "Home Appliances",
"MRP": 120.0,
"Discount": 1.22
}
]📝 The
Discountfield is included for completeness and can be used as an additional value field in the Pivot Table. The minimal report in this sample summarizes only theMRPfield, soDiscountdoes not appear indataSourceSettings.
app.py configures the Flask application, enables CORS, loads product data from the JSON file, and defines REST endpoints for read and CRUD operations:
# filepath: FlaskAPIServer/app.pyfromflaskimportFlask, request, jsonifyfromflask_corsimportCORSimportjsonimportosapp=Flask(__name__)
CORS(app)
DATA_FILE=os.path.join(os.path.dirname(__file__), "products_data.json")
PRIMARY_KEY="ProductID"# Load product data from the JSON file.defload_products():
ifnotos.path.exists(DATA_FILE):
return []
withopen(DATA_FILE, "r", encoding="utf-8") asfile:
returnjson.load(file)
# Store product data in memory.products=load_products()
# GET /products: returns all product records as JSON.@app.get("/products")deflist_products():
returnjsonify(products)
# Start the Flask application.if__name__=="__main__":
app.run(host="127.0.0.1", port=5000, debug=True)🔒 Production CORS: Replace the wildcard
CORS(app)with an explicit origins list, for exampleCORS(app, resources={r"/products*": {"origins": "https://yourdomain.com"}}).🪟 Windows binding tip: The server binds to
127.0.0.1rather thanlocalhostto avoid IPv6 resolution issues on Windows, wherelocalhostmay resolve to::1while the React app connects to127.0.0.1.
The CRUD logic is defined alongside the read endpoint in app.py. Each route corresponds to a standard HTTP method, so the React client can use simple fetch calls.
| HTTP method | Route | Purpose |
|---|---|---|
GET | /products | Retrieve all product records |
POST | /products | Create a new product record |
PUT | /products/<int:item_id> | Update an existing product by ProductID |
DELETE | /products/<int:item_id> | Delete a product by ProductID |
Insert endpoint – POST /products reads the new record from the request body, auto-generates a ProductID when one is not provided, appends it to the in-memory products list, and returns the new record with a 201 Created status.
# filepath: FlaskAPIServer/app.py@app.post("/products")defcreate_product():
row=request.get_json(silent=True) or {}
ifnotrow.get(PRIMARY_KEY):
max_id=max((r.get(PRIMARY_KEY, 0) forrinproducts), default=0)
row[PRIMARY_KEY] =int(max_id) +1products.append(row)
returnjsonify(row), 201Update endpoint – PUT /products/<int:item_id> locates the record by ProductID from the URL, replaces it with the new values, and returns the updated record. Returns 404 if the record is not found.
# filepath: FlaskAPIServer/app.py@app.put("/products/<int:item_id>")defupdate_product(item_id: int):
row=request.get_json(silent=True) or {}
fori, currentinenumerate(products):
ifint(current.get(PRIMARY_KEY)) ==int(item_id):
row[PRIMARY_KEY] =item_idproducts[i] =rowreturnjsonify(row)
returnjsonify({"message": "not found"}), 404Delete endpoint – DELETE /products/<int:item_id> locates the record by ProductID from the URL, removes it from the products list, and returns the deleted record. Returns 404 if the record is not found.
# filepath: FlaskAPIServer/app.py@app.delete("/products/<int:item_id>")defdelete_product(item_id: int):
fori, currentinenumerate(products):
ifint(current.get(PRIMARY_KEY)) ==int(item_id):
deleted=products.pop(i)
returnjsonify(deleted)
returnjsonify({"message": "not found"}), 404
⚠️ Persistence: This sample stores products only in memory. Runtime CRUD changes are discarded when the server restarts (the original contents ofproducts_data.jsonare reloaded on every server start viaload_products()). To persist changes, replace the in-memoryproductslist with logic that writes back toproducts_data.jsonor a database.
The React client lives in the Client/ folder.
cd ../Client
npm installnpm install @syncfusion/ej2-react-pivotview
npm install @syncfusion/ej2-tailwind3-theme🎨 For the Tailwind 3 theme used by the current Getting Started guide, install
@syncfusion/ej2-tailwind3-theme, replace the default contents ofsrc/index.csswith the PivotView theme import documented in the Getting Started guide, and confirm thatsrc/main.tsximportsindex.css. Remove Vite's defaultApp.cssandindex.cssrules if they conflict with the Syncfusion theme.
Open src/App.tsx and ensure the API_BASE constant points to your backend port (default in this repo: 5000).
// filepath: Client/src/App.tsximport*asReactfrom'react';import{PivotViewComponent,Inject,FieldList}from'@syncfusion/ej2-react-pivotview';importtype{DataSourceSettingsModel,CellEditSettings,BeginDrillThroughEventArgs}from'@syncfusion/ej2-react-pivotview';import'./App.css';import{useEffect}from'react';functionApp(): React.ReactElement{constpivotObj=React.useRef<PivotViewComponent>(null);useEffect(()=>{constinitialState={skip: 0};fetchData(initialState).then((data)=>{if(pivotObj.current){pivotObj.current.dataSourceSettings.dataSource=data;}}).catch((e)=>console.error(e));},[]);constAPI_BASE='http://localhost:5000';// Flask server endpoint// --- READ (GET) ---constfetchData=async()=>{consturl=`${API_BASE}/products`;constresponse=awaitfetch(url,{method: 'GET',headers: {'Content-Type': 'application/json'},});if(!response.ok){consttext=awaitresponse.text();thrownewError(`HTTP ${response.status}: ${text}`);}return(awaitresponse.json())asany[];};consthandleActionComplete=async(args: any)=>{try{if(!args||!args.requestType){return;}constsanitizeItem=(item: any)=>{if(!item||typeofitem!=='object'){returnitem;}constsanitized={ ...item};deletesanitized.__index;returnsanitized;};if(args.requestType==='save'&&args.action==='add'){constitem=sanitizeItem(args.data);if(item){constresponse=awaitfetch(`${API_BASE}/products`,{method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify(item),});if(!response.ok){console.error('Create failed',awaitresponse.text());}}return;}if(args.requestType==='save'&&args.action==='edit'){constitem=sanitizeItem(args.data);constid=item?.ProductID??args.primaryKeyValue?.[0]??args.previousData?.ProductID;if(id!=null){constresponse=awaitfetch(`${API_BASE}/products/${id}`,{method: 'PUT',headers: {'Content-Type': 'application/json'},body: JSON.stringify(item),});if(!response.ok){console.error('Update failed',awaitresponse.text());}}return;}if(args.requestType==='delete'){constrows=Array.isArray(args.data) ? args.data : [args.data];for(constrowofrows){if(!row)continue;constid=row?.ProductID;if(id==null)continue;constresponse=awaitfetch(`${API_BASE}/products/${id}`,{method: 'DELETE'});if(!response.ok){console.error('Delete failed',awaitresponse.text());}}}}catch(err){console.error(err);}};constdataSourceSettings: DataSourceSettingsModel={dataSource: [],expandAll: true,rows: [{name: 'ProductName'}],columns: [{name: 'Category'}],values: [{name: 'MRP'}],filters: [],};// Enable editing functionalityconsteditSettings: CellEditSettings={allowEditing: true,// Enables the Edit button and allows users to modify existing records.allowAdding: true,// Enables the Add button and allows users to create new records.allowDeleting: true,// Enables the Delete button and allows users to remove records.mode: 'Normal'// Uses Normal mode (popup dialog) for editing; other options: 'Dialog', 'Batch', 'CommandColumn'.};// Configure beginDrillThrough event to set the primary key for CRUD operationsfunctionbeginDrillThrough(args: BeginDrillThroughEventArgs){// Iterate through all columns in the drill-through gridfor(leti=0;i<args.gridObj.columns.length;i++){// Check if the current column is the primary key columnif(args.gridObj.columns[i].field==="ProductID"){args.gridObj.columns[i].visible=true;// Mark this column as the primary key// This tells DataManager to use this column's value to uniquely identify recordsargs.gridObj.columns[i].isPrimaryKey=true;}}constgridObj=args.gridObj;if(gridObj){gridObj.addEventListener('actionComplete',(event: any)=>{handleActionComplete(event);});}}return(<divclassName='control-section'style={{margin: 100}}><PivotViewComponentref={pivotObj}id='PivotView'height={350}width={700}dataSourceSettings={dataSourceSettings}showFieldList={true}editSettings={editSettings}beginDrillThrough={beginDrillThrough}><Injectservices={[FieldList]}/></PivotViewComponent></div>);}exportdefaultApp;📝 If your Flask server runs on a different port, update the
API_BASEvalue inClient/src/App.tsxaccordingly. The default in this repo is5000.
Code explanation:
useEffect+fetchData– When the component mounts,fetchData()issues aGETrequest to${API_BASE}/productsand assigns the resulting array todataSourceSettings.dataSource, so the Pivot Table renders the data on first paint.handleActionComplete– Listens for the drill-through grid'sactionCompleteevent and dispatches a request to the appropriate Flask endpoint:save+add→POST /productssave+edit→PUT /products/{ProductID}delete→DELETE /products/{ProductID}
sanitizeItem– Strips the internal__indexproperty that the drill-through grid adds to records so only clean product data is sent to the API.dataSourceSettings– Defines the Pivot Table report layout.rows– Displays ProductName values as row headers.columns– Displays Category values as column headers.values– Summarizes the MRP field for each row and column combination.
editSettings– Enables add, edit, and delete operations on the drill-through grid.beginDrillThrough– Marks theProductIDcolumn as the primary key (isPrimaryKey = true) before the drill-through grid is displayed, so update and delete operations target the correct record. The handler also attaches theactionCompletelistener to the grid so CRUD requests are forwarded to the Flask API.FieldList– Displays the Field List and allows fields to be rearranged across rows, columns, values, and filters.
You need two terminals — one for the backend API and one for the React client.
Make sure your virtual environment is activated (see step 2.1), then from the FlaskAPIServer folder run:
python app.pyThe server will start and listen on http://127.0.0.1:5000 by default.
Verify it works:
- 🌐 Open
http://127.0.0.1:5000/productsin your browser, or use a tool like Postman/curl. - ✅ You should see a JSON array of product records.
Sample request via curl:
curl http://127.0.0.1:5000/productsSample response:
[
{ "ProductID": 10001, "ProductName": "Smartwatch", "Category": "Electronics", "MRP": 100.0, "Discount": 1.02 },
{ "ProductID": 10002, "ProductName": "Smartwatch", "Category": "Accessories", "MRP": 110.0, "Discount": 1.12 }
]📝 Note the port number in the terminal output and update the
API_BASEinClient/src/App.tsxif it is different from5000.
cd Client
npm run devThe Vite dev server will start and display a URL (typically http://localhost:5173).
- Open the URL printed by Vite in your browser.
- You should see the Pivot Table populated with aggregated MRP values, grouped by ProductName (rows) and Category (columns).
- Open the browser's Developer Tools (F12) → Network tab.
- Reload the page.
- You should see a
GETrequest tohttp://127.0.0.1:5000/productswith status200and a JSON array containing the product records. - The Pivot Table renders the aggregated data automatically.
The Pivot Table supports full CRUD through its built-in drill-through editing grid.
| Step | Action | Expected Action on Backend |
|---|---|---|
| 1️⃣ | Double-click any pivot cell to open the drill-through grid showing underlying source records. | Initial GET /products (read) |
| ➕ 2️⃣ | Click Add, fill in the new row fields, then click Update. | POST /products |
| ✏️ 3️⃣ | Click Edit on an existing row, change a field, then click Update. | PUT /products/{ProductID} |
| 🗑️ 4️⃣ | Click Delete on a row to remove it. | DELETE /products/{ProductID} |
| 🔁 5️⃣ | Reload the page (or refresh the Pivot Table) to display the updated aggregated data from the backend. | New GET /products (read) |
🔑 The
ProductIDcolumn is automatically marked as the primary key inside thebeginDrillThroughevent, so update and delete operations know which record to target. TheactionCompleteevent handler attached insidebeginDrillThroughforwards each CRUD action to the correct Flask endpoint.
⚠️ Because data is stored only in memory, any CRUD changes made at runtime are discarded when the server is restarted. This is expected behavior for the sample.
| ❓ Issue | 🔍 Symptom | ✅ Resolution |
|---|---|---|
| 🚫 Empty Pivot Table | Pivot loads with no errors but no rows or values appear. | Verify that the Flask endpoint returns data and that the field names returned by the backend match the fields configured in dataSourceSettings (case-sensitive). |
| 🐍 500 Internal Server Error | The Pivot Table fails and the browser shows a server error. | Check the server console for error messages. Verify that products_data.json exists, contains valid JSON, and can be read by the backend. |
| 💥 500 on insert with empty data | Flask returns a 500 error when adding a record. | create_product() computes the new ProductID with max(...) + 1, which fails if the product list is empty. Ensure products_data.json is not empty. |
| 404 Not Found | Updating or deleting a record returns a 404 error. | Verify that the ProductID sent in the request URL matches an existing record and that the record has not already been deleted. |
| 🗑️ Delete request to a wrong endpoint | Delete does not remove the record. | The delete handler expects DELETE /products/{ProductID}. Confirm the React client builds the URL using row.ProductID. |
| 💾 CRUD operations not saving in UI | The edit dialog closes but changes are not reflected in the Pivot Table. | Verify editing is enabled through editSettings and that ProductID is configured as the primary key in the beginDrillThrough event. Also confirm the actionComplete listener is attached inside beginDrillThrough. |
| 🧹 Changes lost after server restart | Records added, updated, or deleted earlier disappear when the Flask server is restarted. | This is expected with the sample backend; the products list lives in memory and is reloaded from products_data.json on every server start. To persist changes, save the modified records to a file or database. |
| 🔄 Changes not reflected in Pivot Table | A CRUD operation completes successfully, but the Pivot Table still shows the old data. | Verify the backend processed the request successfully and returned updated data. Check the browser's Network tab for failed requests. If needed, call pivotObj.current?.refresh(); after an operation. |
| 🌐 CORS Blocked | Console shows Access to XMLHttpRequest ... has been blocked by CORS policy. | Verify CORS(app) is registered in app.py and that flask-cors is installed in the active virtual environment. |
| 🔤 Property casing mismatch | Pivot appears empty or shows "field not found" even though the API returns data. | Ensure field names in the API response match the Pivot Table's dataSourceSettings (e.g., ProductID, ProductName). |
| 🔌 Wrong port | The frontend cannot reach the backend. | Confirm the API_BASE in Client/src/App.tsx matches the port the Flask server is listening on (default 5000). |
🪟 Windows IPv6 / localhost mismatch | Browser connects to localhost but the server is bound to 127.0.0.1, or vice versa. | Bind Flask to 127.0.0.1 (default in this sample) and use http://127.0.0.1:5000 (or localhost, but be consistent) in the React client. |
| 📦 Missing Python packages | The server fails to start with ModuleNotFoundError. | Ensure your virtual environment is activated and pip install flask flask-cors has been run. |
If issues persist, use the browser's Developer Tools (F12) to inspect the Network and Console tabs.
The backend exposes REST endpoints through the Flask routes in app.py. The React client uses the drill-through grid's actionComplete event to call the appropriate endpoint.
| Method | Route | Purpose | Response |
|---|---|---|---|
GET | /products | Retrieve product records (initial load) | JSON array of product records |
POST | /products | Insert a new product | The newly added product record (201) |
PUT | /products/<int:item_id> | Update an existing product by ProductID | The updated product record |
DELETE | /products/<int:item_id> | Delete a product by primary key | The deleted product record |
A product record exposes the following fields:
| Field | Type | Description |
|---|---|---|
ProductID | number | Unique product identifier (primary key) |
ProductName | string | Name of the product |
Category | string | Category to which the product belongs |
MRP | number | Maximum Retail Price of the product |
Discount | number | Discount value applied to the product |
Contributions are welcome and appreciated! 💖
- 🍴 Fork the repository.
- 🌿 Create a feature branch:
git checkout -b feature/my-awesome-change - 💾 Commit your changes:
git commit -m "Add my awesome change" - 📤 Push to your branch:
git push origin feature/my-awesome-change - 🔁 Open a Pull Request describing the change and its motivation.
- Follow the existing code style in both the React and Flask projects.
- Keep changes focused — one feature or fix per pull request.
- Update or add documentation (
README.md,flaskapi-server.md) when behavior changes. - Test your changes locally against both the backend and frontend before submitting.
This project is released under the MIT License. You are free to use, modify, and distribute the code in personal and commercial projects. See the LICENSE file for full text.
- 📘 Documentation:Syncfusion® React Pivot Table Docs
- 💬 Community forum:Syncfusion® Community
- 🐛 Bug reports & feature requests:GitHub Issues
- 📧 Direct support:Syncfusion® Support Portal (for licensed users)
- 📖 Flask Documentation
- 🔀 Flask-CORS Documentation
- 🐍 Python venv Guide
⭐ If this project helped you, please consider giving it a star on GitHub — it helps others discover it!
- 🔗 Syncfusion® React Pivot Table – Getting Started
- 📘 PivotTable Data Binding
- 📘 PivotTable Editing
- 📘 PivotTable Drill-Through
- 🌶️ Flask Documentation
- 🔀 Flask-CORS Documentation
- 🐍 Python venv Guide
- ⚡ Vite Documentation