Latest commit

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Syncfusion® React Pivot Table – Flask API Server Quick Start

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.

ReactFlaskPythonTypeScriptViteSyncfusionLicense


📑 Table of Contents


🚀 Quick Overview

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.

ComponentTechnologyPurpose
🎨 FrontendReact 19 + Vite + Syncfusion® EJ2Render the interactive Pivot Table UI
⚙️ BackendPython 3.11+ + Flask + Flask-CORSServe data, perform CRUD, return JSON responses
🔌 Bindingfetch + useEffect + drill-through actionCompleteBridge between Pivot Table and Flask REST endpoints
📊 Sample DataIn-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.py file containing all routes (GET, POST, PUT, DELETE) and an in-memory data store loaded from products_data.json at startup. CRUD requests from the drill-through grid are dispatched through standard HTTP methods.


✨ Key Features

  • 📊 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 ResponseGET /products returns the product array as plain JSON. The React client assigns it directly to dataSourceSettings.dataSource.
  • 🔑 Primary Key Configuration – Uses ProductID as the primary key for unique record identification during update and delete.
  • 🌐 CORS-Enabled – Preconfigured with Flask-CORS to 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).

🛠️ Prerequisites

Make sure the following software and packages are installed on your machine before running the project.

Software / PackageVersionPurpose
🐍 Python3.11 or laterRuntime for the Flask backend
📦 venvIncluded with PythonCreates an isolated Python environment for the backend
🌶️ Flask2.0 or laterREST API framework
🔀 Flask-CORS3.0 or laterAllows requests from the React dev server
🟢 Node.js20.x LTS or laterRuntime for the React dev server
📦 npm / yarn / pnpmLatest stablePackage manager
⚛️ React19.x or laterBuild the Pivot Table client
⚡ Vite7.3 or laterReact dev server and build tool
📦 @syncfusion/ej2-react-pivotview33.1.45+React Pivot Table component
📦 @syncfusion/ej2-tailwind3-themeLatestSyncfusion theme stylesheet for the Tailwind 3 theme

📂 Project Structure

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

⚙️ Installation & Setup

1. Clone the Repository

git clone https://github.com/SyncfusionExamples/syncfusion-react-pivot-with-flask-api.git
cd syncfusion-react-pivot-with-flask-api

2. Backend – Flask API Server

The backend project lives in the FlaskAPIServer/ folder.

2.1 Create and activate a virtual environment

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/activate

2.2 Install the Python dependencies

pip install flask flask-cors

Package 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.

2.3 Understand the data source

products_data.json provides the in-memory data source for the Pivot Table. It contains product records with the following fields.

FieldData typeDescription
ProductIDnumberUnique product identifier (primary key)
ProductNamestringName of the product
CategorystringCategory to which the product belongs
MRPnumberMaximum Retail Price of the product
DiscountnumberDiscount 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 Discount field 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 the MRP field, so Discount does not appear in dataSourceSettings.

2.4 Inspect the application entry point

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 example CORS(app, resources={r"/products*": {"origins": "https://yourdomain.com"}}).

🪟 Windows binding tip: The server binds to 127.0.0.1 rather than localhost to avoid IPv6 resolution issues on Windows, where localhost may resolve to ::1 while the React app connects to 127.0.0.1.

2.5 Review the CRUD endpoints

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 methodRoutePurpose
GET/productsRetrieve all product records
POST/productsCreate 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 endpointPOST /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), 201

Update endpointPUT /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"}), 404

Delete endpointDELETE /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 of products_data.json are reloaded on every server start via load_products()). To persist changes, replace the in-memory products list with logic that writes back to products_data.json or a database.

3. Frontend – React Pivot Table

The React client lives in the Client/ folder.

3.1 Install npm dependencies

cd ../Client
npm install

3.2 Install the Syncfusion Pivot Table package and theme

npm 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 of src/index.css with the PivotView theme import documented in the Getting Started guide, and confirm that src/main.tsx imports index.css. Remove Vite's default App.css and index.css rules if they conflict with the Syncfusion theme.

3.3 Verify the API URL

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_BASE value in Client/src/App.tsx accordingly. The default in this repo is 5000.

Code explanation:

  • useEffect + fetchData – When the component mounts, fetchData() issues a GET request to ${API_BASE}/products and assigns the resulting array to dataSourceSettings.dataSource, so the Pivot Table renders the data on first paint.
  • handleActionComplete – Listens for the drill-through grid's actionComplete event and dispatches a request to the appropriate Flask endpoint:
    • save + addPOST /products
    • save + editPUT /products/{ProductID}
    • deleteDELETE /products/{ProductID}
  • sanitizeItem – Strips the internal __index property 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 the ProductID column 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 the actionComplete listener 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.

▶️ Running the Application

You need two terminals — one for the backend API and one for the React client.

▶️ Start the Backend (Terminal 1)

Make sure your virtual environment is activated (see step 2.1), then from the FlaskAPIServer folder run:

python app.py

The server will start and listen on http://127.0.0.1:5000 by default.

Verify it works:

  • 🌐 Open http://127.0.0.1:5000/products in 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/products

Sample 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_BASE in Client/src/App.tsx if it is different from 5000.

▶️ Start the Frontend (Terminal 2)

cd Client
npm run dev

The Vite dev server will start and display a URL (typically http://localhost:5173).

✅ Verify in the Browser

  1. Open the URL printed by Vite in your browser.
  2. You should see the Pivot Table populated with aggregated MRP values, grouped by ProductName (rows) and Category (columns).
  3. Open the browser's Developer Tools (F12) → Network tab.
  4. Reload the page.
  5. You should see a GET request to http://127.0.0.1:5000/products with status 200 and a JSON array containing the product records.
  6. The Pivot Table renders the aggregated data automatically.

🧪 Testing CRUD Operations

The Pivot Table supports full CRUD through its built-in drill-through editing grid.

StepActionExpected 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 ProductID column is automatically marked as the primary key inside the beginDrillThrough event, so update and delete operations know which record to target. The actionComplete event handler attached inside beginDrillThrough forwards 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.


🔧 Troubleshooting

❓ Issue🔍 Symptom✅ Resolution
🚫 Empty Pivot TablePivot 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 ErrorThe 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 dataFlask 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 FoundUpdating 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 endpointDelete 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 UIThe 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 restartRecords 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 TableA 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 BlockedConsole 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 mismatchPivot 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 portThe 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 mismatchBrowser 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 packagesThe 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.


📖 API Reference

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.

MethodRoutePurposeResponse
GET/productsRetrieve product records (initial load)JSON array of product records
POST/productsInsert a new productThe newly added product record (201)
PUT/products/<int:item_id>Update an existing product by ProductIDThe updated product record
DELETE/products/<int:item_id>Delete a product by primary keyThe deleted product record

A product record exposes the following fields:

FieldTypeDescription
ProductIDnumberUnique product identifier (primary key)
ProductNamestringName of the product
CategorystringCategory to which the product belongs
MRPnumberMaximum Retail Price of the product
DiscountnumberDiscount value applied to the product

🤝 Contributing

Contributions are welcome and appreciated! 💖

  1. 🍴 Fork the repository.
  2. 🌿 Create a feature branch: git checkout -b feature/my-awesome-change
  3. 💾 Commit your changes: git commit -m "Add my awesome change"
  4. 📤 Push to your branch: git push origin feature/my-awesome-change
  5. 🔁 Open a Pull Request describing the change and its motivation.

📋 Contribution Guidelines

  • 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.

📜 License & Support

📄 License

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.

🛟 Support

⭐ If this project helped you, please consider giving it a star on GitHub — it helps others discover it!


📚 Related Resources


Built with ❤️ using React, Flask, and Python by the Syncfusion® team.

About

This application demonstrates the integration of the Syncfusion React Pivot Table with a Flask API for fetching and processing remote data.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Latest commit

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Syncfusion® React Pivot Table – Flask API Server Quick Start

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.

ReactFlaskPythonTypeScriptViteSyncfusionLicense


📑 Table of Contents


🚀 Quick Overview

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.

ComponentTechnologyPurpose
🎨 FrontendReact 19 + Vite + Syncfusion® EJ2Render the interactive Pivot Table UI
⚙️ BackendPython 3.11+ + Flask + Flask-CORSServe data, perform CRUD, return JSON responses
🔌 Bindingfetch + useEffect + drill-through actionCompleteBridge between Pivot Table and Flask REST endpoints
📊 Sample DataIn-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.py file containing all routes (GET, POST, PUT, DELETE) and an in-memory data store loaded from products_data.json at startup. CRUD requests from the drill-through grid are dispatched through standard HTTP methods.


✨ Key Features

  • 📊 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 ResponseGET /products returns the product array as plain JSON. The React client assigns it directly to dataSourceSettings.dataSource.
  • 🔑 Primary Key Configuration – Uses ProductID as the primary key for unique record identification during update and delete.
  • 🌐 CORS-Enabled – Preconfigured with Flask-CORS to 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).

🛠️ Prerequisites

Make sure the following software and packages are installed on your machine before running the project.

Software / PackageVersionPurpose
🐍 Python3.11 or laterRuntime for the Flask backend
📦 venvIncluded with PythonCreates an isolated Python environment for the backend
🌶️ Flask2.0 or laterREST API framework
🔀 Flask-CORS3.0 or laterAllows requests from the React dev server
🟢 Node.js20.x LTS or laterRuntime for the React dev server
📦 npm / yarn / pnpmLatest stablePackage manager
⚛️ React19.x or laterBuild the Pivot Table client
⚡ Vite7.3 or laterReact dev server and build tool
📦 @syncfusion/ej2-react-pivotview33.1.45+React Pivot Table component
📦 @syncfusion/ej2-tailwind3-themeLatestSyncfusion theme stylesheet for the Tailwind 3 theme

📂 Project Structure

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

⚙️ Installation & Setup

1. Clone the Repository

git clone https://github.com/SyncfusionExamples/syncfusion-react-pivot-with-flask-api.git
cd syncfusion-react-pivot-with-flask-api

2. Backend – Flask API Server

The backend project lives in the FlaskAPIServer/ folder.

2.1 Create and activate a virtual environment

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/activate

2.2 Install the Python dependencies

pip install flask flask-cors

Package 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.

2.3 Understand the data source

products_data.json provides the in-memory data source for the Pivot Table. It contains product records with the following fields.

FieldData typeDescription
ProductIDnumberUnique product identifier (primary key)
ProductNamestringName of the product
CategorystringCategory to which the product belongs
MRPnumberMaximum Retail Price of the product
DiscountnumberDiscount 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 Discount field 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 the MRP field, so Discount does not appear in dataSourceSettings.

2.4 Inspect the application entry point

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 example CORS(app, resources={r"/products*": {"origins": "https://yourdomain.com"}}).

🪟 Windows binding tip: The server binds to 127.0.0.1 rather than localhost to avoid IPv6 resolution issues on Windows, where localhost may resolve to ::1 while the React app connects to 127.0.0.1.

2.5 Review the CRUD endpoints

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 methodRoutePurpose
GET/productsRetrieve all product records
POST/productsCreate 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 endpointPOST /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), 201

Update endpointPUT /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"}), 404

Delete endpointDELETE /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 of products_data.json are reloaded on every server start via load_products()). To persist changes, replace the in-memory products list with logic that writes back to products_data.json or a database.

3. Frontend – React Pivot Table

The React client lives in the Client/ folder.

3.1 Install npm dependencies

cd ../Client
npm install

3.2 Install the Syncfusion Pivot Table package and theme

npm 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 of src/index.css with the PivotView theme import documented in the Getting Started guide, and confirm that src/main.tsx imports index.css. Remove Vite's default App.css and index.css rules if they conflict with the Syncfusion theme.

3.3 Verify the API URL

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_BASE value in Client/src/App.tsx accordingly. The default in this repo is 5000.

Code explanation:

  • useEffect + fetchData – When the component mounts, fetchData() issues a GET request to ${API_BASE}/products and assigns the resulting array to dataSourceSettings.dataSource, so the Pivot Table renders the data on first paint.
  • handleActionComplete – Listens for the drill-through grid's actionComplete event and dispatches a request to the appropriate Flask endpoint:
    • save + addPOST /products
    • save + editPUT /products/{ProductID}
    • deleteDELETE /products/{ProductID}
  • sanitizeItem – Strips the internal __index property 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 the ProductID column 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 the actionComplete listener 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.

▶️ Running the Application

You need two terminals — one for the backend API and one for the React client.

▶️ Start the Backend (Terminal 1)

Make sure your virtual environment is activated (see step 2.1), then from the FlaskAPIServer folder run:

python app.py

The server will start and listen on http://127.0.0.1:5000 by default.

Verify it works:

  • 🌐 Open http://127.0.0.1:5000/products in 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/products

Sample 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_BASE in Client/src/App.tsx if it is different from 5000.

▶️ Start the Frontend (Terminal 2)

cd Client
npm run dev

The Vite dev server will start and display a URL (typically http://localhost:5173).

✅ Verify in the Browser

  1. Open the URL printed by Vite in your browser.
  2. You should see the Pivot Table populated with aggregated MRP values, grouped by ProductName (rows) and Category (columns).
  3. Open the browser's Developer Tools (F12) → Network tab.
  4. Reload the page.
  5. You should see a GET request to http://127.0.0.1:5000/products with status 200 and a JSON array containing the product records.
  6. The Pivot Table renders the aggregated data automatically.

🧪 Testing CRUD Operations

The Pivot Table supports full CRUD through its built-in drill-through editing grid.

StepActionExpected 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 ProductID column is automatically marked as the primary key inside the beginDrillThrough event, so update and delete operations know which record to target. The actionComplete event handler attached inside beginDrillThrough forwards 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.


🔧 Troubleshooting

❓ Issue🔍 Symptom✅ Resolution
🚫 Empty Pivot TablePivot 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 ErrorThe 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 dataFlask 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 FoundUpdating 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 endpointDelete 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 UIThe 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 restartRecords 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 TableA 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 BlockedConsole 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 mismatchPivot 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 portThe 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 mismatchBrowser 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 packagesThe 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.


📖 API Reference

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.

MethodRoutePurposeResponse
GET/productsRetrieve product records (initial load)JSON array of product records
POST/productsInsert a new productThe newly added product record (201)
PUT/products/<int:item_id>Update an existing product by ProductIDThe updated product record
DELETE/products/<int:item_id>Delete a product by primary keyThe deleted product record

A product record exposes the following fields:

FieldTypeDescription
ProductIDnumberUnique product identifier (primary key)
ProductNamestringName of the product
CategorystringCategory to which the product belongs
MRPnumberMaximum Retail Price of the product
DiscountnumberDiscount value applied to the product

🤝 Contributing

Contributions are welcome and appreciated! 💖

  1. 🍴 Fork the repository.
  2. 🌿 Create a feature branch: git checkout -b feature/my-awesome-change
  3. 💾 Commit your changes: git commit -m "Add my awesome change"
  4. 📤 Push to your branch: git push origin feature/my-awesome-change
  5. 🔁 Open a Pull Request describing the change and its motivation.

📋 Contribution Guidelines

  • 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.

📜 License & Support

📄 License

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.

🛟 Support

⭐ If this project helped you, please consider giving it a star on GitHub — it helps others discover it!


📚 Related Resources


Built with ❤️ using React, Flask, and Python by the Syncfusion® team.

About

This application demonstrates the integration of the Syncfusion React Pivot Table with a Flask API for fetching and processing remote data.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Syncfusion® React Pivot Table – Flask API Server Quick Start

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.

ReactFlaskPythonTypeScriptViteSyncfusionLicense


📑 Table of Contents


🚀 Quick Overview

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.

ComponentTechnologyPurpose
🎨 FrontendReact 19 + Vite + Syncfusion® EJ2Render the interactive Pivot Table UI
⚙️ BackendPython 3.11+ + Flask + Flask-CORSServe data, perform CRUD, return JSON responses
🔌 Bindingfetch + useEffect + drill-through actionCompleteBridge between Pivot Table and Flask REST endpoints
📊 Sample DataIn-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.py file containing all routes (GET, POST, PUT, DELETE) and an in-memory data store loaded from products_data.json at startup. CRUD requests from the drill-through grid are dispatched through standard HTTP methods.


✨ Key Features

  • 📊 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 ResponseGET /products returns the product array as plain JSON. The React client assigns it directly to dataSourceSettings.dataSource.
  • 🔑 Primary Key Configuration – Uses ProductID as the primary key for unique record identification during update and delete.
  • 🌐 CORS-Enabled – Preconfigured with Flask-CORS to 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).

🛠️ Prerequisites

Make sure the following software and packages are installed on your machine before running the project.

Software / PackageVersionPurpose
🐍 Python3.11 or laterRuntime for the Flask backend
📦 venvIncluded with PythonCreates an isolated Python environment for the backend
🌶️ Flask2.0 or laterREST API framework
🔀 Flask-CORS3.0 or laterAllows requests from the React dev server
🟢 Node.js20.x LTS or laterRuntime for the React dev server
📦 npm / yarn / pnpmLatest stablePackage manager
⚛️ React19.x or laterBuild the Pivot Table client
⚡ Vite7.3 or laterReact dev server and build tool
📦 @syncfusion/ej2-react-pivotview33.1.45+React Pivot Table component
📦 @syncfusion/ej2-tailwind3-themeLatestSyncfusion theme stylesheet for the Tailwind 3 theme

📂 Project Structure

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

⚙️ Installation & Setup

1. Clone the Repository

git clone https://github.com/SyncfusionExamples/syncfusion-react-pivot-with-flask-api.git
cd syncfusion-react-pivot-with-flask-api

2. Backend – Flask API Server

The backend project lives in the FlaskAPIServer/ folder.

2.1 Create and activate a virtual environment

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/activate

2.2 Install the Python dependencies

pip install flask flask-cors

Package 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.

2.3 Understand the data source

products_data.json provides the in-memory data source for the Pivot Table. It contains product records with the following fields.

FieldData typeDescription
ProductIDnumberUnique product identifier (primary key)
ProductNamestringName of the product
CategorystringCategory to which the product belongs
MRPnumberMaximum Retail Price of the product
DiscountnumberDiscount 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 Discount field 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 the MRP field, so Discount does not appear in dataSourceSettings.

2.4 Inspect the application entry point

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 example CORS(app, resources={r"/products*": {"origins": "https://yourdomain.com"}}).

🪟 Windows binding tip: The server binds to 127.0.0.1 rather than localhost to avoid IPv6 resolution issues on Windows, where localhost may resolve to ::1 while the React app connects to 127.0.0.1.

2.5 Review the CRUD endpoints

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 methodRoutePurpose
GET/productsRetrieve all product records
POST/productsCreate 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 endpointPOST /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), 201

Update endpointPUT /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"}), 404

Delete endpointDELETE /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 of products_data.json are reloaded on every server start via load_products()). To persist changes, replace the in-memory products list with logic that writes back to products_data.json or a database.

3. Frontend – React Pivot Table

The React client lives in the Client/ folder.

3.1 Install npm dependencies

cd ../Client
npm install

3.2 Install the Syncfusion Pivot Table package and theme

npm 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 of src/index.css with the PivotView theme import documented in the Getting Started guide, and confirm that src/main.tsx imports index.css. Remove Vite's default App.css and index.css rules if they conflict with the Syncfusion theme.

3.3 Verify the API URL

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_BASE value in Client/src/App.tsx accordingly. The default in this repo is 5000.

Code explanation:

  • useEffect + fetchData – When the component mounts, fetchData() issues a GET request to ${API_BASE}/products and assigns the resulting array to dataSourceSettings.dataSource, so the Pivot Table renders the data on first paint.
  • handleActionComplete – Listens for the drill-through grid's actionComplete event and dispatches a request to the appropriate Flask endpoint:
    • save + addPOST /products
    • save + editPUT /products/{ProductID}
    • deleteDELETE /products/{ProductID}
  • sanitizeItem – Strips the internal __index property 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 the ProductID column 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 the actionComplete listener 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.

▶️ Running the Application

You need two terminals — one for the backend API and one for the React client.

▶️ Start the Backend (Terminal 1)

Make sure your virtual environment is activated (see step 2.1), then from the FlaskAPIServer folder run:

python app.py

The server will start and listen on http://127.0.0.1:5000 by default.

Verify it works:

  • 🌐 Open http://127.0.0.1:5000/products in 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/products

Sample 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_BASE in Client/src/App.tsx if it is different from 5000.

▶️ Start the Frontend (Terminal 2)

cd Client
npm run dev

The Vite dev server will start and display a URL (typically http://localhost:5173).

✅ Verify in the Browser

  1. Open the URL printed by Vite in your browser.
  2. You should see the Pivot Table populated with aggregated MRP values, grouped by ProductName (rows) and Category (columns).
  3. Open the browser's Developer Tools (F12) → Network tab.
  4. Reload the page.
  5. You should see a GET request to http://127.0.0.1:5000/products with status 200 and a JSON array containing the product records.
  6. The Pivot Table renders the aggregated data automatically.

🧪 Testing CRUD Operations

The Pivot Table supports full CRUD through its built-in drill-through editing grid.

StepActionExpected 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 ProductID column is automatically marked as the primary key inside the beginDrillThrough event, so update and delete operations know which record to target. The actionComplete event handler attached inside beginDrillThrough forwards 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.


🔧 Troubleshooting

❓ Issue🔍 Symptom✅ Resolution
🚫 Empty Pivot TablePivot 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 ErrorThe 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 dataFlask 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 FoundUpdating 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 endpointDelete 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 UIThe 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 restartRecords 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 TableA 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 BlockedConsole 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 mismatchPivot 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 portThe 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 mismatchBrowser 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 packagesThe 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.


📖 API Reference

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.

MethodRoutePurposeResponse
GET/productsRetrieve product records (initial load)JSON array of product records
POST/productsInsert a new productThe newly added product record (201)
PUT/products/<int:item_id>Update an existing product by ProductIDThe updated product record
DELETE/products/<int:item_id>Delete a product by primary keyThe deleted product record

A product record exposes the following fields:

FieldTypeDescription
ProductIDnumberUnique product identifier (primary key)
ProductNamestringName of the product
CategorystringCategory to which the product belongs
MRPnumberMaximum Retail Price of the product
DiscountnumberDiscount value applied to the product

🤝 Contributing

Contributions are welcome and appreciated! 💖

  1. 🍴 Fork the repository.
  2. 🌿 Create a feature branch: git checkout -b feature/my-awesome-change
  3. 💾 Commit your changes: git commit -m "Add my awesome change"
  4. 📤 Push to your branch: git push origin feature/my-awesome-change
  5. 🔁 Open a Pull Request describing the change and its motivation.

📋 Contribution Guidelines

  • 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.

📜 License & Support

📄 License

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.

🛟 Support

⭐ If this project helped you, please consider giving it a star on GitHub — it helps others discover it!


📚 Related Resources


Built with ❤️ using React, Flask, and Python by the Syncfusion® team.

About

This application demonstrates the integration of the Syncfusion React Pivot Table with a Flask API for fetching and processing remote data.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Syncfusion® React Pivot Table – Flask API Server Quick Start

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.

ReactFlaskPythonTypeScriptViteSyncfusionLicense


📑 Table of Contents


🚀 Quick Overview

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.

ComponentTechnologyPurpose
🎨 FrontendReact 19 + Vite + Syncfusion® EJ2Render the interactive Pivot Table UI
⚙️ BackendPython 3.11+ + Flask + Flask-CORSServe data, perform CRUD, return JSON responses
🔌 Bindingfetch + useEffect + drill-through actionCompleteBridge between Pivot Table and Flask REST endpoints
📊 Sample DataIn-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.py file containing all routes (GET, POST, PUT, DELETE) and an in-memory data store loaded from products_data.json at startup. CRUD requests from the drill-through grid are dispatched through standard HTTP methods.


✨ Key Features

  • 📊 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 ResponseGET /products returns the product array as plain JSON. The React client assigns it directly to dataSourceSettings.dataSource.
  • 🔑 Primary Key Configuration – Uses ProductID as the primary key for unique record identification during update and delete.
  • 🌐 CORS-Enabled – Preconfigured with Flask-CORS to 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).

🛠️ Prerequisites

Make sure the following software and packages are installed on your machine before running the project.

Software / PackageVersionPurpose
🐍 Python3.11 or laterRuntime for the Flask backend
📦 venvIncluded with PythonCreates an isolated Python environment for the backend
🌶️ Flask2.0 or laterREST API framework
🔀 Flask-CORS3.0 or laterAllows requests from the React dev server
🟢 Node.js20.x LTS or laterRuntime for the React dev server
📦 npm / yarn / pnpmLatest stablePackage manager
⚛️ React19.x or laterBuild the Pivot Table client
⚡ Vite7.3 or laterReact dev server and build tool
📦 @syncfusion/ej2-react-pivotview33.1.45+React Pivot Table component
📦 @syncfusion/ej2-tailwind3-themeLatestSyncfusion theme stylesheet for the Tailwind 3 theme

📂 Project Structure

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

⚙️ Installation & Setup

1. Clone the Repository

git clone https://github.com/SyncfusionExamples/syncfusion-react-pivot-with-flask-api.git
cd syncfusion-react-pivot-with-flask-api

2. Backend – Flask API Server

The backend project lives in the FlaskAPIServer/ folder.

2.1 Create and activate a virtual environment

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/activate

2.2 Install the Python dependencies

pip install flask flask-cors

Package 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.

2.3 Understand the data source

products_data.json provides the in-memory data source for the Pivot Table. It contains product records with the following fields.

FieldData typeDescription
ProductIDnumberUnique product identifier (primary key)
ProductNamestringName of the product
CategorystringCategory to which the product belongs
MRPnumberMaximum Retail Price of the product
DiscountnumberDiscount 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 Discount field 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 the MRP field, so Discount does not appear in dataSourceSettings.

2.4 Inspect the application entry point

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 example CORS(app, resources={r"/products*": {"origins": "https://yourdomain.com"}}).

🪟 Windows binding tip: The server binds to 127.0.0.1 rather than localhost to avoid IPv6 resolution issues on Windows, where localhost may resolve to ::1 while the React app connects to 127.0.0.1.

2.5 Review the CRUD endpoints

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 methodRoutePurpose
GET/productsRetrieve all product records
POST/productsCreate 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 endpointPOST /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), 201

Update endpointPUT /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"}), 404

Delete endpointDELETE /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 of products_data.json are reloaded on every server start via load_products()). To persist changes, replace the in-memory products list with logic that writes back to products_data.json or a database.

3. Frontend – React Pivot Table

The React client lives in the Client/ folder.

3.1 Install npm dependencies

cd ../Client
npm install

3.2 Install the Syncfusion Pivot Table package and theme

npm 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 of src/index.css with the PivotView theme import documented in the Getting Started guide, and confirm that src/main.tsx imports index.css. Remove Vite's default App.css and index.css rules if they conflict with the Syncfusion theme.

3.3 Verify the API URL

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_BASE value in Client/src/App.tsx accordingly. The default in this repo is 5000.

Code explanation:

  • useEffect + fetchData – When the component mounts, fetchData() issues a GET request to ${API_BASE}/products and assigns the resulting array to dataSourceSettings.dataSource, so the Pivot Table renders the data on first paint.
  • handleActionComplete – Listens for the drill-through grid's actionComplete event and dispatches a request to the appropriate Flask endpoint:
    • save + addPOST /products
    • save + editPUT /products/{ProductID}
    • deleteDELETE /products/{ProductID}
  • sanitizeItem – Strips the internal __index property 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 the ProductID column 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 the actionComplete listener 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.

▶️ Running the Application

You need two terminals — one for the backend API and one for the React client.

▶️ Start the Backend (Terminal 1)

Make sure your virtual environment is activated (see step 2.1), then from the FlaskAPIServer folder run:

python app.py

The server will start and listen on http://127.0.0.1:5000 by default.

Verify it works:

  • 🌐 Open http://127.0.0.1:5000/products in 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/products

Sample 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_BASE in Client/src/App.tsx if it is different from 5000.

▶️ Start the Frontend (Terminal 2)

cd Client
npm run dev

The Vite dev server will start and display a URL (typically http://localhost:5173).

✅ Verify in the Browser

  1. Open the URL printed by Vite in your browser.
  2. You should see the Pivot Table populated with aggregated MRP values, grouped by ProductName (rows) and Category (columns).
  3. Open the browser's Developer Tools (F12) → Network tab.
  4. Reload the page.
  5. You should see a GET request to http://127.0.0.1:5000/products with status 200 and a JSON array containing the product records.
  6. The Pivot Table renders the aggregated data automatically.

🧪 Testing CRUD Operations

The Pivot Table supports full CRUD through its built-in drill-through editing grid.

StepActionExpected 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 ProductID column is automatically marked as the primary key inside the beginDrillThrough event, so update and delete operations know which record to target. The actionComplete event handler attached inside beginDrillThrough forwards 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.


🔧 Troubleshooting

❓ Issue🔍 Symptom✅ Resolution
🚫 Empty Pivot TablePivot 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 ErrorThe 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 dataFlask 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 FoundUpdating 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 endpointDelete 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 UIThe 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 restartRecords 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 TableA 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 BlockedConsole 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 mismatchPivot 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 portThe 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 mismatchBrowser 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 packagesThe 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.


📖 API Reference

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.

MethodRoutePurposeResponse
GET/productsRetrieve product records (initial load)JSON array of product records
POST/productsInsert a new productThe newly added product record (201)
PUT/products/<int:item_id>Update an existing product by ProductIDThe updated product record
DELETE/products/<int:item_id>Delete a product by primary keyThe deleted product record

A product record exposes the following fields:

FieldTypeDescription
ProductIDnumberUnique product identifier (primary key)
ProductNamestringName of the product
CategorystringCategory to which the product belongs
MRPnumberMaximum Retail Price of the product
DiscountnumberDiscount value applied to the product

🤝 Contributing

Contributions are welcome and appreciated! 💖

  1. 🍴 Fork the repository.
  2. 🌿 Create a feature branch: git checkout -b feature/my-awesome-change
  3. 💾 Commit your changes: git commit -m "Add my awesome change"
  4. 📤 Push to your branch: git push origin feature/my-awesome-change
  5. 🔁 Open a Pull Request describing the change and its motivation.

📋 Contribution Guidelines

  • 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.

📜 License & Support

📄 License

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.

🛟 Support

⭐ If this project helped you, please consider giving it a star on GitHub — it helps others discover it!


📚 Related Resources


Built with ❤️ using React, Flask, and Python by the Syncfusion® team.

About

This application demonstrates the integration of the Syncfusion React Pivot Table with a Flask API for fetching and processing remote data.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Latest commit

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Syncfusion® React Pivot Table – Flask API Server Quick Start

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.

ReactFlaskPythonTypeScriptViteSyncfusionLicense


📑 Table of Contents


🚀 Quick Overview

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.

ComponentTechnologyPurpose
🎨 FrontendReact 19 + Vite + Syncfusion® EJ2Render the interactive Pivot Table UI
⚙️ BackendPython 3.11+ + Flask + Flask-CORSServe data, perform CRUD, return JSON responses
🔌 Bindingfetch + useEffect + drill-through actionCompleteBridge between Pivot Table and Flask REST endpoints
📊 Sample DataIn-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.py file containing all routes (GET, POST, PUT, DELETE) and an in-memory data store loaded from products_data.json at startup. CRUD requests from the drill-through grid are dispatched through standard HTTP methods.


✨ Key Features

  • 📊 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 ResponseGET /products returns the product array as plain JSON. The React client assigns it directly to dataSourceSettings.dataSource.
  • 🔑 Primary Key Configuration – Uses ProductID as the primary key for unique record identification during update and delete.
  • 🌐 CORS-Enabled – Preconfigured with Flask-CORS to 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).

🛠️ Prerequisites

Make sure the following software and packages are installed on your machine before running the project.

Software / PackageVersionPurpose
🐍 Python3.11 or laterRuntime for the Flask backend
📦 venvIncluded with PythonCreates an isolated Python environment for the backend
🌶️ Flask2.0 or laterREST API framework
🔀 Flask-CORS3.0 or laterAllows requests from the React dev server
🟢 Node.js20.x LTS or laterRuntime for the React dev server
📦 npm / yarn / pnpmLatest stablePackage manager
⚛️ React19.x or laterBuild the Pivot Table client
⚡ Vite7.3 or laterReact dev server and build tool
📦 @syncfusion/ej2-react-pivotview33.1.45+React Pivot Table component
📦 @syncfusion/ej2-tailwind3-themeLatestSyncfusion theme stylesheet for the Tailwind 3 theme

📂 Project Structure

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

⚙️ Installation & Setup

1. Clone the Repository

git clone https://github.com/SyncfusionExamples/syncfusion-react-pivot-with-flask-api.git
cd syncfusion-react-pivot-with-flask-api

2. Backend – Flask API Server

The backend project lives in the FlaskAPIServer/ folder.

2.1 Create and activate a virtual environment

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/activate

2.2 Install the Python dependencies

pip install flask flask-cors

Package 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.

2.3 Understand the data source

products_data.json provides the in-memory data source for the Pivot Table. It contains product records with the following fields.

FieldData typeDescription
ProductIDnumberUnique product identifier (primary key)
ProductNamestringName of the product
CategorystringCategory to which the product belongs
MRPnumberMaximum Retail Price of the product
DiscountnumberDiscount 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 Discount field 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 the MRP field, so Discount does not appear in dataSourceSettings.

2.4 Inspect the application entry point

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 example CORS(app, resources={r"/products*": {"origins": "https://yourdomain.com"}}).

🪟 Windows binding tip: The server binds to 127.0.0.1 rather than localhost to avoid IPv6 resolution issues on Windows, where localhost may resolve to ::1 while the React app connects to 127.0.0.1.

2.5 Review the CRUD endpoints

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 methodRoutePurpose
GET/productsRetrieve all product records
POST/productsCreate 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 endpointPOST /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), 201

Update endpointPUT /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"}), 404

Delete endpointDELETE /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 of products_data.json are reloaded on every server start via load_products()). To persist changes, replace the in-memory products list with logic that writes back to products_data.json or a database.

3. Frontend – React Pivot Table

The React client lives in the Client/ folder.

3.1 Install npm dependencies

cd ../Client
npm install

3.2 Install the Syncfusion Pivot Table package and theme

npm 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 of src/index.css with the PivotView theme import documented in the Getting Started guide, and confirm that src/main.tsx imports index.css. Remove Vite's default App.css and index.css rules if they conflict with the Syncfusion theme.

3.3 Verify the API URL

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_BASE value in Client/src/App.tsx accordingly. The default in this repo is 5000.

Code explanation:

  • useEffect + fetchData – When the component mounts, fetchData() issues a GET request to ${API_BASE}/products and assigns the resulting array to dataSourceSettings.dataSource, so the Pivot Table renders the data on first paint.
  • handleActionComplete – Listens for the drill-through grid's actionComplete event and dispatches a request to the appropriate Flask endpoint:
    • save + addPOST /products
    • save + editPUT /products/{ProductID}
    • deleteDELETE /products/{ProductID}
  • sanitizeItem – Strips the internal __index property 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 the ProductID column 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 the actionComplete listener 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.

▶️ Running the Application

You need two terminals — one for the backend API and one for the React client.

▶️ Start the Backend (Terminal 1)

Make sure your virtual environment is activated (see step 2.1), then from the FlaskAPIServer folder run:

python app.py

The server will start and listen on http://127.0.0.1:5000 by default.

Verify it works:

  • 🌐 Open http://127.0.0.1:5000/products in 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/products

Sample 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_BASE in Client/src/App.tsx if it is different from 5000.

▶️ Start the Frontend (Terminal 2)

cd Client
npm run dev

The Vite dev server will start and display a URL (typically http://localhost:5173).

✅ Verify in the Browser

  1. Open the URL printed by Vite in your browser.
  2. You should see the Pivot Table populated with aggregated MRP values, grouped by ProductName (rows) and Category (columns).
  3. Open the browser's Developer Tools (F12) → Network tab.
  4. Reload the page.
  5. You should see a GET request to http://127.0.0.1:5000/products with status 200 and a JSON array containing the product records.
  6. The Pivot Table renders the aggregated data automatically.

🧪 Testing CRUD Operations

The Pivot Table supports full CRUD through its built-in drill-through editing grid.

StepActionExpected 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 ProductID column is automatically marked as the primary key inside the beginDrillThrough event, so update and delete operations know which record to target. The actionComplete event handler attached inside beginDrillThrough forwards 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.


🔧 Troubleshooting

❓ Issue🔍 Symptom✅ Resolution
🚫 Empty Pivot TablePivot 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 ErrorThe 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 dataFlask 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 FoundUpdating 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 endpointDelete 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 UIThe 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 restartRecords 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 TableA 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 BlockedConsole 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 mismatchPivot 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 portThe 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 mismatchBrowser 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 packagesThe 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.


📖 API Reference

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.

MethodRoutePurposeResponse
GET/productsRetrieve product records (initial load)JSON array of product records
POST/productsInsert a new productThe newly added product record (201)
PUT/products/<int:item_id>Update an existing product by ProductIDThe updated product record
DELETE/products/<int:item_id>Delete a product by primary keyThe deleted product record

A product record exposes the following fields:

FieldTypeDescription
ProductIDnumberUnique product identifier (primary key)
ProductNamestringName of the product
CategorystringCategory to which the product belongs
MRPnumberMaximum Retail Price of the product
DiscountnumberDiscount value applied to the product

🤝 Contributing

Contributions are welcome and appreciated! 💖

  1. 🍴 Fork the repository.
  2. 🌿 Create a feature branch: git checkout -b feature/my-awesome-change
  3. 💾 Commit your changes: git commit -m "Add my awesome change"
  4. 📤 Push to your branch: git push origin feature/my-awesome-change
  5. 🔁 Open a Pull Request describing the change and its motivation.

📋 Contribution Guidelines

  • 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.

📜 License & Support

📄 License

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.

🛟 Support

⭐ If this project helped you, please consider giving it a star on GitHub — it helps others discover it!


📚 Related Resources


Built with ❤️ using React, Flask, and Python by the Syncfusion® team.

About

This application demonstrates the integration of the Syncfusion React Pivot Table with a Flask API for fetching and processing remote data.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Syncfusion® React Pivot Table – Flask API Server Quick Start

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.

ReactFlaskPythonTypeScriptViteSyncfusionLicense


📑 Table of Contents


🚀 Quick Overview

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.

ComponentTechnologyPurpose
🎨 FrontendReact 19 + Vite + Syncfusion® EJ2Render the interactive Pivot Table UI
⚙️ BackendPython 3.11+ + Flask + Flask-CORSServe data, perform CRUD, return JSON responses
🔌 Bindingfetch + useEffect + drill-through actionCompleteBridge between Pivot Table and Flask REST endpoints
📊 Sample DataIn-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.py file containing all routes (GET, POST, PUT, DELETE) and an in-memory data store loaded from products_data.json at startup. CRUD requests from the drill-through grid are dispatched through standard HTTP methods.


✨ Key Features

  • 📊 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 ResponseGET /products returns the product array as plain JSON. The React client assigns it directly to dataSourceSettings.dataSource.
  • 🔑 Primary Key Configuration – Uses ProductID as the primary key for unique record identification during update and delete.
  • 🌐 CORS-Enabled – Preconfigured with Flask-CORS to 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).

🛠️ Prerequisites

Make sure the following software and packages are installed on your machine before running the project.

Software / PackageVersionPurpose
🐍 Python3.11 or laterRuntime for the Flask backend
📦 venvIncluded with PythonCreates an isolated Python environment for the backend
🌶️ Flask2.0 or laterREST API framework
🔀 Flask-CORS3.0 or laterAllows requests from the React dev server
🟢 Node.js20.x LTS or laterRuntime for the React dev server
📦 npm / yarn / pnpmLatest stablePackage manager
⚛️ React19.x or laterBuild the Pivot Table client
⚡ Vite7.3 or laterReact dev server and build tool
📦 @syncfusion/ej2-react-pivotview33.1.45+React Pivot Table component
📦 @syncfusion/ej2-tailwind3-themeLatestSyncfusion theme stylesheet for the Tailwind 3 theme

📂 Project Structure

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

⚙️ Installation & Setup

1. Clone the Repository

git clone https://github.com/SyncfusionExamples/syncfusion-react-pivot-with-flask-api.git
cd syncfusion-react-pivot-with-flask-api

2. Backend – Flask API Server

The backend project lives in the FlaskAPIServer/ folder.

2.1 Create and activate a virtual environment

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/activate

2.2 Install the Python dependencies

pip install flask flask-cors

Package 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.

2.3 Understand the data source

products_data.json provides the in-memory data source for the Pivot Table. It contains product records with the following fields.

FieldData typeDescription
ProductIDnumberUnique product identifier (primary key)
ProductNamestringName of the product
CategorystringCategory to which the product belongs
MRPnumberMaximum Retail Price of the product
DiscountnumberDiscount 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 Discount field 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 the MRP field, so Discount does not appear in dataSourceSettings.

2.4 Inspect the application entry point

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 example CORS(app, resources={r"/products*": {"origins": "https://yourdomain.com"}}).

🪟 Windows binding tip: The server binds to 127.0.0.1 rather than localhost to avoid IPv6 resolution issues on Windows, where localhost may resolve to ::1 while the React app connects to 127.0.0.1.

2.5 Review the CRUD endpoints

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 methodRoutePurpose
GET/productsRetrieve all product records
POST/productsCreate 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 endpointPOST /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), 201

Update endpointPUT /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"}), 404

Delete endpointDELETE /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 of products_data.json are reloaded on every server start via load_products()). To persist changes, replace the in-memory products list with logic that writes back to products_data.json or a database.

3. Frontend – React Pivot Table

The React client lives in the Client/ folder.

3.1 Install npm dependencies

cd ../Client
npm install

3.2 Install the Syncfusion Pivot Table package and theme

npm 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 of src/index.css with the PivotView theme import documented in the Getting Started guide, and confirm that src/main.tsx imports index.css. Remove Vite's default App.css and index.css rules if they conflict with the Syncfusion theme.

3.3 Verify the API URL

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_BASE value in Client/src/App.tsx accordingly. The default in this repo is 5000.

Code explanation:

  • useEffect + fetchData – When the component mounts, fetchData() issues a GET request to ${API_BASE}/products and assigns the resulting array to dataSourceSettings.dataSource, so the Pivot Table renders the data on first paint.
  • handleActionComplete – Listens for the drill-through grid's actionComplete event and dispatches a request to the appropriate Flask endpoint:
    • save + addPOST /products
    • save + editPUT /products/{ProductID}
    • deleteDELETE /products/{ProductID}
  • sanitizeItem – Strips the internal __index property 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 the ProductID column 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 the actionComplete listener 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.

▶️ Running the Application

You need two terminals — one for the backend API and one for the React client.

▶️ Start the Backend (Terminal 1)

Make sure your virtual environment is activated (see step 2.1), then from the FlaskAPIServer folder run:

python app.py

The server will start and listen on http://127.0.0.1:5000 by default.

Verify it works:

  • 🌐 Open http://127.0.0.1:5000/products in 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/products

Sample 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_BASE in Client/src/App.tsx if it is different from 5000.

▶️ Start the Frontend (Terminal 2)

cd Client
npm run dev

The Vite dev server will start and display a URL (typically http://localhost:5173).

✅ Verify in the Browser

  1. Open the URL printed by Vite in your browser.
  2. You should see the Pivot Table populated with aggregated MRP values, grouped by ProductName (rows) and Category (columns).
  3. Open the browser's Developer Tools (F12) → Network tab.
  4. Reload the page.
  5. You should see a GET request to http://127.0.0.1:5000/products with status 200 and a JSON array containing the product records.
  6. The Pivot Table renders the aggregated data automatically.

🧪 Testing CRUD Operations

The Pivot Table supports full CRUD through its built-in drill-through editing grid.

StepActionExpected 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 ProductID column is automatically marked as the primary key inside the beginDrillThrough event, so update and delete operations know which record to target. The actionComplete event handler attached inside beginDrillThrough forwards 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.


🔧 Troubleshooting

❓ Issue🔍 Symptom✅ Resolution
🚫 Empty Pivot TablePivot 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 ErrorThe 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 dataFlask 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 FoundUpdating 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 endpointDelete 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 UIThe 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 restartRecords 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 TableA 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 BlockedConsole 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 mismatchPivot 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 portThe 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 mismatchBrowser 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 packagesThe 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.


📖 API Reference

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.

MethodRoutePurposeResponse
GET/productsRetrieve product records (initial load)JSON array of product records
POST/productsInsert a new productThe newly added product record (201)
PUT/products/<int:item_id>Update an existing product by ProductIDThe updated product record
DELETE/products/<int:item_id>Delete a product by primary keyThe deleted product record

A product record exposes the following fields:

FieldTypeDescription
ProductIDnumberUnique product identifier (primary key)
ProductNamestringName of the product
CategorystringCategory to which the product belongs
MRPnumberMaximum Retail Price of the product
DiscountnumberDiscount value applied to the product

🤝 Contributing

Contributions are welcome and appreciated! 💖

  1. 🍴 Fork the repository.
  2. 🌿 Create a feature branch: git checkout -b feature/my-awesome-change
  3. 💾 Commit your changes: git commit -m "Add my awesome change"
  4. 📤 Push to your branch: git push origin feature/my-awesome-change
  5. 🔁 Open a Pull Request describing the change and its motivation.

📋 Contribution Guidelines

  • 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.

📜 License & Support

📄 License

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.

🛟 Support

⭐ If this project helped you, please consider giving it a star on GitHub — it helps others discover it!


📚 Related Resources


Built with ❤️ using React, Flask, and Python by the Syncfusion® team.

About

This application demonstrates the integration of the Syncfusion React Pivot Table with a Flask API for fetching and processing remote data.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Syncfusion® React Pivot Table – Flask API Server Quick Start

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.

ReactFlaskPythonTypeScriptViteSyncfusionLicense


📑 Table of Contents


🚀 Quick Overview

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.

ComponentTechnologyPurpose
🎨 FrontendReact 19 + Vite + Syncfusion® EJ2Render the interactive Pivot Table UI
⚙️ BackendPython 3.11+ + Flask + Flask-CORSServe data, perform CRUD, return JSON responses
🔌 Bindingfetch + useEffect + drill-through actionCompleteBridge between Pivot Table and Flask REST endpoints
📊 Sample DataIn-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.py file containing all routes (GET, POST, PUT, DELETE) and an in-memory data store loaded from products_data.json at startup. CRUD requests from the drill-through grid are dispatched through standard HTTP methods.


✨ Key Features

  • 📊 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 ResponseGET /products returns the product array as plain JSON. The React client assigns it directly to dataSourceSettings.dataSource.
  • 🔑 Primary Key Configuration – Uses ProductID as the primary key for unique record identification during update and delete.
  • 🌐 CORS-Enabled – Preconfigured with Flask-CORS to 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).

🛠️ Prerequisites

Make sure the following software and packages are installed on your machine before running the project.

Software / PackageVersionPurpose
🐍 Python3.11 or laterRuntime for the Flask backend
📦 venvIncluded with PythonCreates an isolated Python environment for the backend
🌶️ Flask2.0 or laterREST API framework
🔀 Flask-CORS3.0 or laterAllows requests from the React dev server
🟢 Node.js20.x LTS or laterRuntime for the React dev server
📦 npm / yarn / pnpmLatest stablePackage manager
⚛️ React19.x or laterBuild the Pivot Table client
⚡ Vite7.3 or laterReact dev server and build tool
📦 @syncfusion/ej2-react-pivotview33.1.45+React Pivot Table component
📦 @syncfusion/ej2-tailwind3-themeLatestSyncfusion theme stylesheet for the Tailwind 3 theme

📂 Project Structure

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

⚙️ Installation & Setup

1. Clone the Repository

git clone https://github.com/SyncfusionExamples/syncfusion-react-pivot-with-flask-api.git
cd syncfusion-react-pivot-with-flask-api

2. Backend – Flask API Server

The backend project lives in the FlaskAPIServer/ folder.

2.1 Create and activate a virtual environment

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/activate

2.2 Install the Python dependencies

pip install flask flask-cors

Package 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.

2.3 Understand the data source

products_data.json provides the in-memory data source for the Pivot Table. It contains product records with the following fields.

FieldData typeDescription
ProductIDnumberUnique product identifier (primary key)
ProductNamestringName of the product
CategorystringCategory to which the product belongs
MRPnumberMaximum Retail Price of the product
DiscountnumberDiscount 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 Discount field 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 the MRP field, so Discount does not appear in dataSourceSettings.

2.4 Inspect the application entry point

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 example CORS(app, resources={r"/products*": {"origins": "https://yourdomain.com"}}).

🪟 Windows binding tip: The server binds to 127.0.0.1 rather than localhost to avoid IPv6 resolution issues on Windows, where localhost may resolve to ::1 while the React app connects to 127.0.0.1.

2.5 Review the CRUD endpoints

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 methodRoutePurpose
GET/productsRetrieve all product records
POST/productsCreate 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 endpointPOST /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), 201

Update endpointPUT /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"}), 404

Delete endpointDELETE /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 of products_data.json are reloaded on every server start via load_products()). To persist changes, replace the in-memory products list with logic that writes back to products_data.json or a database.

3. Frontend – React Pivot Table

The React client lives in the Client/ folder.

3.1 Install npm dependencies

cd ../Client
npm install

3.2 Install the Syncfusion Pivot Table package and theme

npm 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 of src/index.css with the PivotView theme import documented in the Getting Started guide, and confirm that src/main.tsx imports index.css. Remove Vite's default App.css and index.css rules if they conflict with the Syncfusion theme.

3.3 Verify the API URL

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_BASE value in Client/src/App.tsx accordingly. The default in this repo is 5000.

Code explanation:

  • useEffect + fetchData – When the component mounts, fetchData() issues a GET request to ${API_BASE}/products and assigns the resulting array to dataSourceSettings.dataSource, so the Pivot Table renders the data on first paint.
  • handleActionComplete – Listens for the drill-through grid's actionComplete event and dispatches a request to the appropriate Flask endpoint:
    • save + addPOST /products
    • save + editPUT /products/{ProductID}
    • deleteDELETE /products/{ProductID}
  • sanitizeItem – Strips the internal __index property 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 the ProductID column 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 the actionComplete listener 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.

▶️ Running the Application

You need two terminals — one for the backend API and one for the React client.

▶️ Start the Backend (Terminal 1)

Make sure your virtual environment is activated (see step 2.1), then from the FlaskAPIServer folder run:

python app.py

The server will start and listen on http://127.0.0.1:5000 by default.

Verify it works:

  • 🌐 Open http://127.0.0.1:5000/products in 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/products

Sample 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_BASE in Client/src/App.tsx if it is different from 5000.

▶️ Start the Frontend (Terminal 2)

cd Client
npm run dev

The Vite dev server will start and display a URL (typically http://localhost:5173).

✅ Verify in the Browser

  1. Open the URL printed by Vite in your browser.
  2. You should see the Pivot Table populated with aggregated MRP values, grouped by ProductName (rows) and Category (columns).
  3. Open the browser's Developer Tools (F12) → Network tab.
  4. Reload the page.
  5. You should see a GET request to http://127.0.0.1:5000/products with status 200 and a JSON array containing the product records.
  6. The Pivot Table renders the aggregated data automatically.

🧪 Testing CRUD Operations

The Pivot Table supports full CRUD through its built-in drill-through editing grid.

StepActionExpected 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 ProductID column is automatically marked as the primary key inside the beginDrillThrough event, so update and delete operations know which record to target. The actionComplete event handler attached inside beginDrillThrough forwards 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.


🔧 Troubleshooting

❓ Issue🔍 Symptom✅ Resolution
🚫 Empty Pivot TablePivot 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 ErrorThe 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 dataFlask 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 FoundUpdating 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 endpointDelete 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 UIThe 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 restartRecords 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 TableA 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 BlockedConsole 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 mismatchPivot 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 portThe 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 mismatchBrowser 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 packagesThe 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.


📖 API Reference

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.

MethodRoutePurposeResponse
GET/productsRetrieve product records (initial load)JSON array of product records
POST/productsInsert a new productThe newly added product record (201)
PUT/products/<int:item_id>Update an existing product by ProductIDThe updated product record
DELETE/products/<int:item_id>Delete a product by primary keyThe deleted product record

A product record exposes the following fields:

FieldTypeDescription
ProductIDnumberUnique product identifier (primary key)
ProductNamestringName of the product
CategorystringCategory to which the product belongs
MRPnumberMaximum Retail Price of the product
DiscountnumberDiscount value applied to the product

🤝 Contributing

Contributions are welcome and appreciated! 💖

  1. 🍴 Fork the repository.
  2. 🌿 Create a feature branch: git checkout -b feature/my-awesome-change
  3. 💾 Commit your changes: git commit -m "Add my awesome change"
  4. 📤 Push to your branch: git push origin feature/my-awesome-change
  5. 🔁 Open a Pull Request describing the change and its motivation.

📋 Contribution Guidelines

  • 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.

📜 License & Support

📄 License

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.

🛟 Support

⭐ If this project helped you, please consider giving it a star on GitHub — it helps others discover it!


📚 Related Resources


Built with ❤️ using React, Flask, and Python by the Syncfusion® team.

About

This application demonstrates the integration of the Syncfusion React Pivot Table with a Flask API for fetching and processing remote data.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Latest commit

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Syncfusion® React Pivot Table – Flask API Server Quick Start

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.

ReactFlaskPythonTypeScriptViteSyncfusionLicense


📑 Table of Contents


🚀 Quick Overview

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.

ComponentTechnologyPurpose
🎨 FrontendReact 19 + Vite + Syncfusion® EJ2Render the interactive Pivot Table UI
⚙️ BackendPython 3.11+ + Flask + Flask-CORSServe data, perform CRUD, return JSON responses
🔌 Bindingfetch + useEffect + drill-through actionCompleteBridge between Pivot Table and Flask REST endpoints
📊 Sample DataIn-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.py file containing all routes (GET, POST, PUT, DELETE) and an in-memory data store loaded from products_data.json at startup. CRUD requests from the drill-through grid are dispatched through standard HTTP methods.


✨ Key Features

  • 📊 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 ResponseGET /products returns the product array as plain JSON. The React client assigns it directly to dataSourceSettings.dataSource.
  • 🔑 Primary Key Configuration – Uses ProductID as the primary key for unique record identification during update and delete.
  • 🌐 CORS-Enabled – Preconfigured with Flask-CORS to 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).

🛠️ Prerequisites

Make sure the following software and packages are installed on your machine before running the project.

Software / PackageVersionPurpose
🐍 Python3.11 or laterRuntime for the Flask backend
📦 venvIncluded with PythonCreates an isolated Python environment for the backend
🌶️ Flask2.0 or laterREST API framework
🔀 Flask-CORS3.0 or laterAllows requests from the React dev server
🟢 Node.js20.x LTS or laterRuntime for the React dev server
📦 npm / yarn / pnpmLatest stablePackage manager
⚛️ React19.x or laterBuild the Pivot Table client
⚡ Vite7.3 or laterReact dev server and build tool
📦 @syncfusion/ej2-react-pivotview33.1.45+React Pivot Table component
📦 @syncfusion/ej2-tailwind3-themeLatestSyncfusion theme stylesheet for the Tailwind 3 theme

📂 Project Structure

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

⚙️ Installation & Setup

1. Clone the Repository

git clone https://github.com/SyncfusionExamples/syncfusion-react-pivot-with-flask-api.git
cd syncfusion-react-pivot-with-flask-api

2. Backend – Flask API Server

The backend project lives in the FlaskAPIServer/ folder.

2.1 Create and activate a virtual environment

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/activate

2.2 Install the Python dependencies

pip install flask flask-cors

Package 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.

2.3 Understand the data source

products_data.json provides the in-memory data source for the Pivot Table. It contains product records with the following fields.

FieldData typeDescription
ProductIDnumberUnique product identifier (primary key)
ProductNamestringName of the product
CategorystringCategory to which the product belongs
MRPnumberMaximum Retail Price of the product
DiscountnumberDiscount 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 Discount field 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 the MRP field, so Discount does not appear in dataSourceSettings.

2.4 Inspect the application entry point

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 example CORS(app, resources={r"/products*": {"origins": "https://yourdomain.com"}}).

🪟 Windows binding tip: The server binds to 127.0.0.1 rather than localhost to avoid IPv6 resolution issues on Windows, where localhost may resolve to ::1 while the React app connects to 127.0.0.1.

2.5 Review the CRUD endpoints

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 methodRoutePurpose
GET/productsRetrieve all product records
POST/productsCreate 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 endpointPOST /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), 201

Update endpointPUT /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"}), 404

Delete endpointDELETE /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 of products_data.json are reloaded on every server start via load_products()). To persist changes, replace the in-memory products list with logic that writes back to products_data.json or a database.

3. Frontend – React Pivot Table

The React client lives in the Client/ folder.

3.1 Install npm dependencies

cd ../Client
npm install

3.2 Install the Syncfusion Pivot Table package and theme

npm 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 of src/index.css with the PivotView theme import documented in the Getting Started guide, and confirm that src/main.tsx imports index.css. Remove Vite's default App.css and index.css rules if they conflict with the Syncfusion theme.

3.3 Verify the API URL

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_BASE value in Client/src/App.tsx accordingly. The default in this repo is 5000.

Code explanation:

  • useEffect + fetchData – When the component mounts, fetchData() issues a GET request to ${API_BASE}/products and assigns the resulting array to dataSourceSettings.dataSource, so the Pivot Table renders the data on first paint.
  • handleActionComplete – Listens for the drill-through grid's actionComplete event and dispatches a request to the appropriate Flask endpoint:
    • save + addPOST /products
    • save + editPUT /products/{ProductID}
    • deleteDELETE /products/{ProductID}
  • sanitizeItem – Strips the internal __index property 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 the ProductID column 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 the actionComplete listener 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.

▶️ Running the Application

You need two terminals — one for the backend API and one for the React client.

▶️ Start the Backend (Terminal 1)

Make sure your virtual environment is activated (see step 2.1), then from the FlaskAPIServer folder run:

python app.py

The server will start and listen on http://127.0.0.1:5000 by default.

Verify it works:

  • 🌐 Open http://127.0.0.1:5000/products in 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/products

Sample 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_BASE in Client/src/App.tsx if it is different from 5000.

▶️ Start the Frontend (Terminal 2)

cd Client
npm run dev

The Vite dev server will start and display a URL (typically http://localhost:5173).

✅ Verify in the Browser

  1. Open the URL printed by Vite in your browser.
  2. You should see the Pivot Table populated with aggregated MRP values, grouped by ProductName (rows) and Category (columns).
  3. Open the browser's Developer Tools (F12) → Network tab.
  4. Reload the page.
  5. You should see a GET request to http://127.0.0.1:5000/products with status 200 and a JSON array containing the product records.
  6. The Pivot Table renders the aggregated data automatically.

🧪 Testing CRUD Operations

The Pivot Table supports full CRUD through its built-in drill-through editing grid.

StepActionExpected 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 ProductID column is automatically marked as the primary key inside the beginDrillThrough event, so update and delete operations know which record to target. The actionComplete event handler attached inside beginDrillThrough forwards 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.


🔧 Troubleshooting

❓ Issue🔍 Symptom✅ Resolution
🚫 Empty Pivot TablePivot 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 ErrorThe 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 dataFlask 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 FoundUpdating 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 endpointDelete 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 UIThe 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 restartRecords 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 TableA 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 BlockedConsole 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 mismatchPivot 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 portThe 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 mismatchBrowser 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 packagesThe 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.


📖 API Reference

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.

MethodRoutePurposeResponse
GET/productsRetrieve product records (initial load)JSON array of product records
POST/productsInsert a new productThe newly added product record (201)
PUT/products/<int:item_id>Update an existing product by ProductIDThe updated product record
DELETE/products/<int:item_id>Delete a product by primary keyThe deleted product record

A product record exposes the following fields:

FieldTypeDescription
ProductIDnumberUnique product identifier (primary key)
ProductNamestringName of the product
CategorystringCategory to which the product belongs
MRPnumberMaximum Retail Price of the product
DiscountnumberDiscount value applied to the product

🤝 Contributing

Contributions are welcome and appreciated! 💖

  1. 🍴 Fork the repository.
  2. 🌿 Create a feature branch: git checkout -b feature/my-awesome-change
  3. 💾 Commit your changes: git commit -m "Add my awesome change"
  4. 📤 Push to your branch: git push origin feature/my-awesome-change
  5. 🔁 Open a Pull Request describing the change and its motivation.

📋 Contribution Guidelines

  • 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.

📜 License & Support

📄 License

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.

🛟 Support

⭐ If this project helped you, please consider giving it a star on GitHub — it helps others discover it!


📚 Related Resources


Built with ❤️ using React, Flask, and Python by the Syncfusion® team.

About

This application demonstrates the integration of the Syncfusion React Pivot Table with a Flask API for fetching and processing remote data.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages