
Syncfusion React Data Manager
- 357 installs
- 3 repo stars
- Updated July 28, 2026
- syncfusion/react-ui-components-skills
Use syncfusion-react-data-manager for development tasks
About
syncfusion-react-data-manager: A skill for development. This provides functionality for development workflows.
- syncfusion-react-data-manager
Syncfusion React Data Manager by the numbers
- 357 all-time installs (skills.sh)
- +22 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #1,140 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Jul 29, 2026 (Skillselion catalog sync)
npx skills add https://github.com/syncfusion/react-ui-components-skills --skill syncfusion-react-data-managerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 357 |
|---|---|
| repo stars | ★ 3 |
| Last updated | July 28, 2026 |
| Repository | syncfusion/react-ui-components-skills ↗ |
What it does
Use syncfusion-react-data-manager for development tasks
Files
Syncfusion React DataManager
The DataManager is a gateway component for managing local and remote data operations in React applications. It acts as an abstraction layer that enables seamless data binding, querying, CRUD operations, caching, and middleware customization without writing complex server communication code.
Use this skill whenever a user needs to:
- Bind local or remote data sources to UI components
- Query data (filter, sort, page, group, search)
- Perform CRUD operations (insert, update, delete, batch edits)
- Work with different data sources (JSON, OData, REST APIs, GraphQL, etc.)
- Cache data for performance
- Implement offline mode
- Add custom headers or authentication
- Transform request/response data
- Handle complex data scenarios in React applications
⚠️ Security & Trust Boundary
- This skill generates code only, the agent does not execute data operations or fetch remote endpoints — all DataManager interactions occur solely within the user's application at runtime.
- Generated code must treat all third-party API responses as untrusted input, never bind to unvalidated or user-provided URLs, and ensure authentication is enforced on all remote endpoints.
When to Use DataManager
DataManager is best for:
- Flexible data management → Works with any data source (local, REST, OData, GraphQL)
- Complex queries → Filtering, sorting, paging, grouping, searching
- CRUD operations → With batch support and transaction patterns
- Middleware customization → Pre/post request hooks
- Offline support → Cache and sync when connection restored
- Adaptor switching → Change data sources without code changes
Direct component binding (Grid, Scheduler, etc.) is better when:
- You only need basic data binding without complex querying
- Performance is critical for very large datasets
- You don't need middleware customization
Key Concepts
Data Binding
Connect DataManager to data sources using the json property (local arrays) or url property (remote APIs). Query methods: executeLocal() for local data, executeQuery() for remote data.
Query Class
Build structured queries using the Query class: from(), where(), select(), sortBy(), take(), expand(), group() chained together for powerful data operations.
Adaptors
Adaptors are interfaces that enable DataManager to communicate with different data sources:
- JsonAdaptor → Local JavaScript arrays
- ODataAdaptor → OData v3 services
- ODataV4Adaptor → OData v4 services
- UrlAdaptor → Generic REST endpoints
- WebApiAdaptor → ASP.NET Web API
- RemoteSaveAdaptor → Hybrid client-side queries + server-side CRUD
- WebMethodAdaptor → Legacy ASP.NET ASMX services
- GraphQLAdaptor → GraphQL endpoints
- CustomAdaptor → Custom implementation for proprietary services
CRUD Operations
- insert() → Add records to data source
- update() → Modify existing records (keyField as first parameter)
- remove() → Delete records (keyField as first parameter)
- saveChanges() → Batch multiple operations into single request
Middleware & Customization
- applyPreRequestMiddlewares() → Modify request before sending (auth tokens, headers)
- applyPostRequestMiddlewares() → Transform response before binding (formatting, filtering)
- Custom adaptors for proprietary APIs
- Error handlers for request failures
Caching & Performance
- enableCache → Prevent re-requesting previously loaded pages
- Offline mode → Store data locally, sync when connection restored
- Load on demand → Lazy load data as user scrolls (virtual scrolling)
- Deferred operations → Promise-based async/await patterns
Documentation & Navigation Guide
Choose the reference file based on your current task:
Getting Started Setup
📄 Read: references/getting-started.md
- Installation steps (npm)
- Project setup & imports
- Basic DataManager initialization
- First query execution
- Environment setup
- Common errors & fixes
Data Binding Strategies
📄 Read: references/data-binding.md
- Local data binding with json property
- Remote data binding with url property
- executeLocal() for client-side queries
- executeQuery() for server requests
- Switching between data sources
- Response format handling
- When to use each approach
Query Operations & Filtering
📄 Read: references/querying-and-filtering.md
- Query class fundamentals
- Filtering with where() and Predicate
- Sorting (ascending/descending)
- Pagination with take() and skip()
- Field projection with select()
- Related data loading with expand()
- Grouping data by field(s)
- Complex multi-condition queries
- Chaining query operations
CRUD Operations & Batch Edits
📄 Read: references/crud-operations.md
- insert() → Add new records
- update() → Modify existing records
- remove() → Delete records
- keyField in CRUD method parameters
- saveChanges() for batch operations
- Batch insert/update/remove
- Transaction patterns
- Data validation before operations
- Response handling
Choosing & Using Adaptors
📄 Read: references/adaptors-guide.md
- Which adaptor to use (decision tree)
- JsonAdaptor for local arrays
- OData adaptors (v3 & v4)
- REST API adaptors (Url, WebApi)
- RemoteSaveAdaptor hybrid approach
- Legacy ASMX (WebMethodAdaptor)
- GraphQL endpoint integration
- Custom adaptor implementation
- CORS & crossDomain configuration
- Platform-specific response handling
Middleware & Request Customization
📄 Read: references/middleware-customization.md
- Pre-request middleware for auth tokens
- Custom header injection
- Request transformation
- Post-request middleware for response mapping
- Error handler functions
- Data validation & filtering in middleware
- Custom adaptor implementation
- Middleware execution order & timing
Caching & Offline Mode
📄 Read: references/caching-offline-mode.md
- enableCache property for performance
- When cache clears (sort, filter, CRUD)
- Offline mode implementation
- Local storage persistence
- Sync strategies after reconnection
- Cache size & memory management
- Best practices & performance patterns
Advanced Features & Patterns
📄 Read: references/advanced-features.md
- Load on demand & pagination patterns
- Virtual scrolling setup
- Lazy loading data
- Deferred operations (Promise API)
- Async/await with DataManager
- State persistence across sessions
- Memory management
- Error handling strategies
- Performance optimization tips
- Common pitfalls & solutions
Quick Start Example
import React, { useEffect, useState } from 'react';
import { DataManager, WebApiAdaptor, Query } from '@syncfusion/ej2-data';
export default function DataManagerDemo() {
const [items, setItems] = useState([]);
useEffect(() => {
// Create DataManager with remote data source
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor(),
crossDomain: true
});
// Execute query with filtering, sorting, paging
dataManager.executeQuery(
new Query()
.where('Freight', 'greaterthan', 500) // Filter
.sortBy('OrderDate') // Sort
.take(10) // Pagination
).then((e) => {
setItems(e.result); // Bind results
});
}, []);
return (
<div>
<h2>Orders</h2>
<ul>
{items.map((item) => (
<li key={item.OrderID}>{item.CustomerID} - ${item.Freight}</li>
))}
</ul>
</div>
);
}Common Use Cases
Case 1: Display local data with filtering → Use JsonAdaptor + Query + where() for client-side filtering
Case 2: Fetch server data with authentication → Use WebApiAdaptor + applyPreRequestMiddlewares() for token injection
Case 3: Work offline, sync later → Use enableCache + offline mode + saveChanges() for batch sync
Case 4: Complex nested data relationships → Use ODataV4Adaptor or RemoteSaveAdaptor + expand() for related data
Case 5: GraphQL backend → Use GraphQLAdaptor with custom query builder
Case 6: Legacy ASP.NET ASMX service → Use WebMethodAdaptor with appropriate response mapping
Key Props & Options
| Property | Type | Purpose |
|---|---|---|
| json | Array | Local JavaScript array of objects |
| url | string | Remote server endpoint URL |
| adaptor | Adaptor | JsonAdaptor, ODataAdaptor, WebApiAdaptor, etc. |
| enableCache | boolean | Cache pages to prevent redundant requests |
| offline | boolean | Enable offline mode with local storage |
| crossDomain | boolean | CORS requests to different domain |
Comparison: DataManager vs Direct Binding
| Feature | DataManager | Direct Binding |
|---|---|---|
| Flexible adaptors | ✓ Easy switching | ✗ Fixed to one source |
| Complex queries | ✓ Query class with chaining | ✗ Limited filtering |
| CRUD operations | ✓ Full support + batch | ✗ Basic operations |
| Middleware | ✓ Pre/post hooks | ✗ None |
| Caching | ✓ Built-in | ✗ Manual implementation |
| Offline mode | ✓ Built-in | ✗ Manual implementation |
| Performance (small datasets) | ≈ Same | ✓ Slightly faster |
| Performance (large datasets) | ✓ Lazy loading support | ✗ All at once |
Use DataManager when building data-driven applications with flexibility, caching, and middleware needs.
Use direct binding for simple, one-off displays without complex queries or offline support.
Adaptors Guide for React DataManager
Table of Contents
- Adaptor Overview
- Decision Tree: Which Adaptor to Use
- Adaptor Comparison
- JsonAdaptor
- ODataAdaptor
- ODataV4Adaptor
- UrlAdaptor
- WebApiAdaptor
- RemoteSaveAdaptor
- WebMethodAdaptor
- GraphQLAdaptor
- CustomDataAdaptor
- CustomAdaptor
- CORS & crossDomain Configuration
---
Adaptor Overview
An adaptor is an interface that enables DataManager to communicate with different types of data sources. Each adaptor handles:
- Request formatting
- Response parsing
- Paging/filtering/sorting communication
- Error handling
- Platform-specific conventions
Choose the adaptor that matches your backend service.
---
Decision Tree: Which Adaptor to Use
Is your data source?
├─ JavaScript array (in-memory)
│ └─ Use: JsonAdaptor
├─ REST API
│ ├─ Need full control over request/response?
│ │ └─ FIRST CHOICE: CustomDataAdaptor ⭐ (MOST FLEXIBLE)
│ │ └─ Use ANY fetching tool (fetch, axios, Socket.IO, etc.)
│ │ └─ Return standard: { result, count }
│ │
│ ├─ ASP.NET Web API?
│ │ └─ Use: WebApiAdaptor
│ ├─ Generic REST (GET/POST standard)?
│ │ └─ Use: UrlAdaptor
│ ├─ Need client + server queries?
│ │ └─ Use: RemoteSaveAdaptor
│ └─ Still need more control after choosing above?
│ └─ SECONDARY CHOICE: CustomAdaptor (Extend built-in)
├─ OData Service?
│ ├─ OData v3?
│ │ └─ Use: ODataAdaptor
│ ├─ OData v4?
│ │ └─ Use: ODataV4Adaptor
├─ GraphQL Endpoint?
│ └─ Use: GraphQLAdaptor
├─ Legacy ASMX Web Service?
│ └─ Use: WebMethodAdaptor
└─ None of above?
└─ Use CustomDataAdaptor (covers 95% custom scenarios)---
Adaptor Comparison
| Feature | Json | OData v3 | OData v4 | Url | WebApi | RemoteSave | WebMethod | GraphQL | CustomData | Custom |
|---|---|---|---|---|---|---|---|---|---|---|
| Local data | ✓ | - | - | - | - | - | - | - | - | - |
| Remote API | - | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Query support | ✓ Client | ✓ Server | ✓ Server | ✓ Server | ✓ Server | ✓ Both* | Limited | ✓ Server | ✓ Custom | ✓ Custom |
| CRUD | ✓ Local | ✓ Server | ✓ Server | ✓ Server | ✓ Server | ✓ Server | ✓ Server | ✓ Server | ✓ Server | ✓ Server |
| Batch CRUD | - | Limited | ✓ | Limited | ✓ | ✓ | - | - | ✓ | ✓ |
| Paging | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Limited | ✓ | ✓ | ✓ |
| Sorting | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Limited | ✓ | ✓ | ✓ |
*RemoteSaveAdaptor: Client-side queries (filter/sort/page) + Server-side CRUD
---
JsonAdaptor
Use case: Local JavaScript arrays, no server needed
import React, { useEffect, useState } from 'react';
import { DataManager, JsonAdaptor, Query } from '@syncfusion/ej2-data';
export default function JsonAdaptorExample() {
const [results, setResults] = useState([]);
useEffect(() => {
// Local data
const dataArray = [
{ OrderID: 10248, CustomerID: 'VINET', Freight: 32.38 },
{ OrderID: 10249, CustomerID: 'TOMSP', Freight: 11.61 },
{ OrderID: 10250, CustomerID: 'HANAR', Freight: 65.83 },
];
// No URL needed - data is in memory
const dataManager = new DataManager(dataArray);
// Client-side query execution
dataManager.executeLocal(
new Query()
.where('Freight', 'greaterthan', 30)
.sortBy('CustomerID')
.take(10)
).then((e) => {
setResults(e.result);
});
}, []);
return <pre>{JSON.stringify(results, null, 2)}</pre>;
}When to use:
- Small datasets (< 10,000 records)
- All data available in browser
- Quick prototyping
- Offline-first applications
---
ODataAdaptor
Use case: OData v3 services (older ASP.NET services, SharePoint)
import { DataManager, ODataAdaptor, Query } from '@syncfusion/ej2-data';
const dataManager = new DataManager({
url: 'url',
adaptor: new ODataAdaptor(),
crossDomain: true
});
dataManager.executeQuery(
new Query()
.from('Orders')
.where('Freight', 'lessthan', 100)
.take(10)
).then((e) => {
console.log(e.result); // OData v3 response
});Response format:
{
"d": {
"results": [
{ "OrderID": 10248, "CustomerID": "VINET", "Freight": 32.38 }
],
"__count": "100"
}
}---
ODataV4Adaptor
Use case: OData v4 services (newer Microsoft services, modern backends)
import { DataManager, ODataV4Adaptor, Query } from '@syncfusion/ej2-data';
const dataManager = new DataManager({
url: 'url',
adaptor: new ODataV4Adaptor(),
crossDomain: true
});
// Query syntax same, but response format is OData v4
dataManager.executeQuery(
new Query()
.from('Orders')
.where('Freight', 'greaterthan', 50)
.select(['OrderID', 'CustomerID', 'Freight'])
.take(20)
).then((e) => {
console.log(e.result);
});
// Expand related data
const expandQuery = new Query()
.from('Orders')
.expand('Customer')
.take(10);Response format:
{
"value": [
{ "OrderID": 10248, "CustomerID": "VINET", "Freight": 32.38 }
],
"@odata.count": 100
}Key differences from v3:
valuearray instead ofd.results@odata.countinstead of__count- Better support for
expand() - Richer query syntax
---
UrlAdaptor
Use case: Generic REST APIs without standard conventions. Important: UrlAdaptor sends only POST requests with query parameters in the request body - never GET.
import { DataManager, UrlAdaptor, Query } from '@syncfusion/ej2-data';
const dataManager = new DataManager({
url: 'url',
adaptor: new UrlAdaptor(),
crossDomain: true
});
dataManager.executeQuery(
new Query()
.where('status', 'equal', 'shipped')
.take(10)
.skip(0)
).then((e) => {
console.log(e.result);
});Expected server response:
{
"result": [
{ "id": 1, "name": "Order 1" },
{ "id": 2, "name": "Order 2" }
],
"count": 100
}Query parameters sent (POST only):
POST url
Body: { skip: 0, take: 10, where: [...{field: Status, operator: equal, value: 'active'}], $sorted: [{field: Name, direction: Ascending"}] }---
WebApiAdaptor
Use case: ASP.NET Web API projects (standard REST conventions)
import { DataManager, WebApiAdaptor, Query } from '@syncfusion/ej2-data';
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor(),
crossDomain: true
});
// CRUD operations specify keyField as parameter
dataManager.update('Orders', recordData, { key: 10248 });
// Works with ASP.NET OData conventions
dataManager.executeQuery(
new Query()
.where('Freight', 'greaterthan', 50)
.take(10)
).then((e) => {
console.log(e.result);
});
// CRUD operations send proper HTTP methods
await dataManager.insert(newOrder, 'Orders'); // POST
await dataManager.update('OrderID', updatedOrder, 'Orders'); // PUT
await dataManager.remove('OrderID', 10248, 'Orders'); // DELETEExpected server response:
{
"Items": [
{ "id": 1, "name": "Order 1" },
{ "id": 2, "name": "Order 2" }
],
"Count": 100
}HTTP methods:
- GET: Queries
- POST: Insert
- PUT: Update
- DELETE: Delete
---
RemoteSaveAdaptor
Use case: Hybrid approach - client-side filtering/sorting but server-side CRUD
import { DataManager, RemoteSaveAdaptor, Query } from '@syncfusion/ej2-data';
const dataManager = new DataManager({
url: 'url',
adaptor: new RemoteSaveAdaptor(),
crossDomain: true,
insertUrl: 'url/insert', // Executed Automaticaly
updateUrl: 'url/update', // Executed Automaticaly
removeUrl: 'url/delete', // Executed Automaticaly
});
// Executed LOCALLY (filtering, sorting happens in browser)
dataManager.executeLocal(
new Query()
.where('Freight', 'greaterthan', 50)
.sortByDesc('OrderDate')
.take(10)
).then((e) => {
console.log(e.result);
});
// CRUD sent to SERVER (specify keyField as parameter)
await dataManager.insert(newOrder, 'Orders'); // Server handles insert
await dataManager.update('OrderID', order, 'Orders'); // Server handles update
// Initial data fetch
const query = new Query().take(1000); // Load all/most dataWhen to use:
- Need client-side filtering for performance
- Server handles updates only
- Cache all data locally
- Avoid repeated server requests
---
WebMethodAdaptor
Use case: Legacy ASP.NET ASMX Web Services
import { DataManager, WebMethodAdaptor, Query } from '@syncfusion/ej2-data';
const dataManager = new DataManager({
url: 'url',
adaptor: new WebMethodAdaptor(),
crossDomain: true
});
dataManager.executeQuery(
new Query()
.where('Status', 'equal', 'Pending')
.take(20)
).then((e) => {
console.log(e.result);
});Note: Limited support for advanced queries. Use for legacy systems only.
---
GraphQLAdaptor
Use case: GraphQL endpoints
import { DataManager, GraphQLAdaptor, Query } from '@syncfusion/ej2-data';
const dataManager = new DataManager({
url: 'url',
adaptor: new GraphQLAdaptor(),
crossDomain: true
});
// Convert Query to GraphQL format
dataManager.executeQuery(
new Query()
.select(['id', 'name', 'email'])
.where('status', 'equal', 'active')
.take(10)
).then((e) => {
console.log(e.result);
});GraphQL query generated:
query {
orders(
where: { status: "active" }
first: 10
) {
id
name
email
}
}---
CustomDataAdaptor
Use case: Custom request/response formats with full control over data transformation
import { DataManager, CustomDataAdaptor, Query } from '@syncfusion/ej2-data';
class MyCustomAdaptor extends CustomDataAdaptor {
getData(args, query) {
if (args.action === 'read') {
// Custom fetch pattern
const params = {
pageNum: Math.ceil(args.skip / args.take) + 1,
pageSize: args.take,
search: query?.params?.search || ''
};
return fetch('url/data', {
method: 'POST'
})
.then(response => response.json())
.then(data => new Promise((resolve) => {
// Transform backend format to DataManager format
resolve({
result: data.items,
count: data.total
});
}));
}
}
}
const dataManager = new DataManager({
adaptor: new MyCustomAdaptor()
});
dataManager.executeQuery(new Query().take(10)).then((e) => {
console.log('Custom data:', e.result);
});When to use CustomDataAdaptor:
- API returns non-standard format
- Need complete control over request/response
- Complex data transformation required
- Backend uses proprietary JSON structure
---
CustomAdaptor
Use case: Proprietary or complex APIs requiring custom logic
import { DataManager, Adaptor } from '@syncfusion/ej2-data';
class CustomAdaptorExample extends Adaptor {
constructor() {
super();
this.baseUrl = 'url';
}
// Handle query processing
processQuery = (dataManager: DataManager, query: Query, hierarchyPreference: any) => {
// Custom logic: transform Query to API format
const params = {
filter: query.queries.filter(q => q.fn === 'where'),
sort: query.queries.filter(q => q.fn === 'sortBy'),
page: query.pageIndex,
pageSize: query.pageSize
};
return {
type: 'GET',
url: `${this.baseUrl}/orders`,
data: params
};
};
// Override beforeSend to add headers
beforeSend(dm: DataManager, request: XMLHttpRequest | Request, settings?: any): void {
super.beforeSend(dm, request, settings);
if (request instanceof XMLHttpRequest) {
request.setRequestHeader('Authorization', `send_token`);
request.setRequestHeader('X-API-Key', 'apikey');
}
}
// Override processResponse to transform data
processResponse(response: any, dm?: DataManager, query?: Query, xhr?: XMLHttpRequest, request?: Request, params?: any): any {
const result = super.processResponse(response, dm, query, xhr, request, params);
// Transform result
if (result && result.result) {
result.result = result.result.map((item: any) => ({
...item,
processed: true
}));
}
return result;
}
// Handle insert
insert = (dataManager: DataManager, data, tableName) => {
return {
type: 'POST',
url: `${this.baseUrl}/${tableName}`,
data: JSON.stringify(data),
contentType: 'application/json'
};
};
// Handle update
update = (dataManager: DataManager, keyField, value, tableName) => {
return {
type: 'PATCH',
url: `${this.baseUrl}/${tableName}/${value[keyField]}`,
data: JSON.stringify(value),
contentType: 'application/json'
};
};
// Handle delete
remove = (dataManager: DataManager, keyField, value, tableName) => {
return {
type: 'DELETE',
url: `${this.baseUrl}/${tableName}/${value}`
};
};
}
// Use in DataManager
const dataManager = new DataManager({
adaptor: new CustomAdaptorExample(),
crossDomain: true
});When to create CustomAdaptor:
- APIs with non-standard conventions
- Complex request/response mapping
- Authentication requirements
- Special error handling
---
CORS & crossDomain Configuration
Most remote adaptors need CORS handling:
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor(),
crossDomain: true // Enable CORS requests
});CORS errors occur when:
- Frontend and backend on different domains
- Backend doesn't allow cross-origin requests
Solutions: 1. Enable on backend: Add CORS headers 2. Set crossDomain: true in DataManager 3. Use JSONP: For older browsers (limited support) 4. Proxy through same domain: API gateway
---
Platform-Specific Configuration
ASP.NET Web API
new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
// Pass keyField in CRUD operations:
// dm.update('OrderID', data, 'Orders')
})Node.js Express
new DataManager({
url: 'url',
adaptor: new UrlAdaptor(),
crossDomain: true
})OData Service (any platform)
new DataManager({
url: 'url',
adaptor: new ODataV4Adaptor(),
crossDomain: true
})GraphQL (Apollo, Hasura, etc.)
new DataManager({
url: 'url',
adaptor: new GraphQLAdaptor(),
crossDomain: true
})Advanced Features in React DataManager
Table of Contents
- Load on Demand & Pagination
- Virtual Scrolling
- Lazy Loading with Expand
- Deferred Operations
- Async/Await Patterns
- State Persistence
- Memory Management
- Error Handling Strategies
- Performance Optimization Tips
- Common Pitfalls
---
Load on Demand & Pagination
Implement pagination to load data incrementally instead of all at once:
import React, { useEffect, useState } from 'react';
import { DataManager, WebApiAdaptor, Query } from '@syncfusion/ej2-data';
export default function PaginationExample() {
const [orders, setOrders] = useState([]);
const [pageSize] = useState(10);
const [currentPage, setCurrentPage] = useState(1);
const [totalRecords, setTotalRecords] = useState(0);
useEffect(() => {
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
// Calculate skip
const skip = (currentPage - 1) * pageSize;
// Load current page
dataManager.executeQuery(
new Query()
.skip(skip)
.take(pageSize)
).then((e) => {
setOrders(e.result);
setTotalRecords(e.count);
});
}, [currentPage, pageSize]);
const totalPages = Math.ceil(totalRecords / pageSize);
return (
<div>
<table>
<tbody>
{orders.map((order) => (
<tr key={order.OrderID}>
<td>{order.OrderID}</td>
<td>{order.CustomerID}</td>
</tr>
))}
</tbody>
</table>
<nav>
<button
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
disabled={currentPage === 1}
>
Previous
</button>
<span>Page {currentPage} of {totalPages}</span>
<button
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
disabled={currentPage === totalPages}
>
Next
</button>
</nav>
</div>
);
}Benefits:
- Reduced initial load time
- Lower bandwidth usage
- Better for large datasets (100k+ records)
- Smoother user experience
---
Virtual Scrolling
Render only visible rows for extremely large datasets:
// Virtual scrolling with DataManager
import { DataManager, WebApiAdaptor, Query } from '@syncfusion/ej2-data';
export default function VirtualScrollingExample() {
const [visibleRows, setVisibleRows] = useState([]);
const pageSize = 10;
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor(),
enableCache: true // Cache for performance
});
const handleScroll = (e) => {
const { scrollTop, clientHeight } = e.target;
const scrollPercent = scrollTop / (e.target.scrollHeight - clientHeight);
if (scrollPercent > 0.8) {
// User scrolled 80% - load next page
loadNextPage();
}
};
const loadNextPage = async () => {
const currentCount = visibleRows.length;
const result = await dataManager.executeQuery(
new Query()
.skip(currentCount)
.take(pageSize)
);
setVisibleRows([...visibleRows, ...result.result]);
};
return (
<div onScroll={handleScroll} style={{ height: '600px', overflow: 'auto' }}>
{visibleRows.map((row) => (
<div key={row.OrderID}>{row.CustomerID}</div>
))}
</div>
);
}Use virtual scrolling when:
- 10,000+ records to display
- Need smooth scrolling without lag
- Working on mobile devices
- Memory usage is critical
---
Lazy Loading with Expand
Load related data only when needed:
import { DataManager, Query, ODataV4Adaptor } from '@syncfusion/ej2-data';
const dataManager = new DataManager({
url: 'url',
adaptor: new ODataV4Adaptor()
});
// Basic query (no related data)
const basicQuery = new Query()
.take(20);
dataManager.executeQuery(basicQuery).then((e) => {
// { OrderID, CustomerID, ... } but no Customer details
console.log('Basic:', e.result);
});
// With expand (includes related Customer)
const expandedQuery = new Query()
.expand('Customer') // Lazy load related data
.take(20);
dataManager.executeQuery(expandedQuery).then((e) => {
// { OrderID, CustomerID, ..., Customer: { CompanyName, ... } }
console.log('With expand:', e.result[0].Customer);
});
// Multiple related entities
const multiExpandQuery = new Query()
.expand('Customer')
.expand('Employee')
.take(20);
dataManager.executeQuery(multiExpandQuery).then((e) => {
console.log('Multiple expand:', e.result[0]);
});Key points:
expand()only works with OData services that support navigation properties- Lazy loading reduces initial payload size
- Related data comes nested in the result object
- Use multiple
expand()calls for multiple relationships
---
Deferred Operations
The Deferred object provides methods for managing asynchronous operations with proper error handling using .resolve(), .reject(), .then(), and .catch():
import { DataManager, Deferred, Query, WebApiAdaptor } from '@syncfusion/ej2-data';
// Create a deferred object for async operation management
const deferred = new Deferred();
// Simulate async work (e.g., fetching from API)
setTimeout(() => {
const result = {
result: [
{ id: 1, name: 'Order 1' },
{ id: 2, name: 'Order 2' }
],
count: 2
};
deferred.resolve(result); // Mark operation as successful
}, 1000);
// Handle the deferred operation result
deferred.promise
.then((result) => {
console.log('Operation succeeded:', result.result);
})
.catch((error) => {
console.error('Operation failed:', error.message);
});Deferred with Rejection (Error Handling)
import { DataManager, Deferred } from '@syncfusion/ej2-data';
function fetchDataWithError(shouldFail = false) {
const deferred = new Deferred();
setTimeout(() => {
if (shouldFail) {
deferred.reject(new Error('API returned 500 Server Error'));
} else {
deferred.resolve({ result: [{ id: 1, value: 'Success' }] });
}
}, 1000);
return deferred;
}
// Handling rejected deferred
fetchDataWithError(true)
.promise
.then((result) => {
console.log('Success:', result.result);
})
.catch((error) => {
console.error('Error caught:', error.message); // "API returned 500 Server Error"
});Deferred with Query Execution
import { DataManager, Query, WebApiAdaptor } from '@syncfusion/ej2-data';
const dm = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
// executeQuery() returns a Deferred internally
const deferred = dm.executeQuery(
new Query().where('Status', 'equal', 'Completed').take(10)
);
deferred
.then((result) => {
console.log('Query succeeded:', result.result);
return result.result;
})
.catch((error) => {
console.error('Query failed:', error);
});Multiple Deferred Operations (Parallel Execution)
import { Deferred } from '@syncfusion/ej2-data';
const deferred1 = new Deferred();
const deferred2 = new Deferred();
const deferred3 = new Deferred();
// Simulate multiple async operations
setTimeout(() => deferred1.resolve({ data: 'Result 1' }), 500);
setTimeout(() => deferred2.resolve({ data: 'Result 2' }), 700);
setTimeout(() => deferred3.resolve({ data: 'Result 3' }), 600);
// Wait for all deferred operations to complete
Promise.all([deferred1.promise, deferred2.promise, deferred3.promise])
.then((results) => {
console.log('All operations complete:', results);
})
.catch((error) => {
console.error('One or more operations failed:', error);
});---
Async/Await Patterns
Modern async/await syntax for cleaner Promise handling with proper imports:
import { DataManager, WebApiAdaptor, Query } from '@syncfusion/ej2-data';
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor(),
keyColumn: 'OrderID'
});
// AVOID: Without async/await (callback nesting/pyramid of doom)
dataManager.executeQuery(new Query().take(10)).then((e1) => {
dataManager.executeQuery(new Query().skip(10).take(10)).then((e2) => {
dataManager.executeQuery(new Query().skip(20).take(10)).then((e3) => {
console.log('Nested results:', e1.result, e2.result, e3.result);
}).catch((error) => console.error('Query 3 error:', error));
}).catch((error) => console.error('Query 2 error:', error));
}).catch((error) => console.error('Query 1 error:', error));
// RECOMMENDED: With async/await (sequential execution)
const loadDataSequential = async () => {
try {
// Execute queries one after another
const result1 = await dataManager.executeQuery(new Query().take(10));
const result2 = await dataManager.executeQuery(new Query().skip(10).take(10));
const result3 = await dataManager.executeQuery(new Query().skip(20).take(10));
console.log('Sequential results:', result1.result, result2.result, result3.result);
} catch (error) {
if (error.status === 401) {
console.error('Unauthorized - refresh token');
} else if (error.status === 500) {
console.error('Server error - retry later');
} else {
console.error('Query failed:', error.message);
}
}
};
await loadDataSequential();
// BEST: Parallel async operations with Promise.all()
const loadDataParallel = async () => {
try {
// Execute all queries simultaneously
const [result1, result2, result3] = await Promise.all([
dataManager.executeQuery(new Query().take(10)),
dataManager.executeQuery(new Query().skip(10).take(10)),
dataManager.executeQuery(new Query().skip(20).take(10))
]);
console.log('Parallel results:', result1.result, result2.result, result3.result);
} catch (error) {
console.error('One or more queries failed:', error);
}
};
await loadDataParallel();---
State Persistence
Syncfusion DataManager provides built-in state persistence using browser localStorage. Query states are automatically saved and restored across page reloads.
Basic State Persistence
import React, { useEffect, useState } from 'react';
import { DataManager, WebApiAdaptor, Query } from '@syncfusion/ej2-data';
export default function StatePersistenceExample() {
const [orders, setOrders] = useState([]);
useEffect(() => {
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor(),
enablePersistence: true, // Enable state persistence
id: 'reactOrderManager' // Unique ID for localStorage key
});
// Execute a query with sorting
dataManager.executeQuery(
new Query().sortBy('CustomerID', 'descending').take(10)
).then((result) => {
setOrders(result.result);
// Query state (sorting) automatically saved to localStorage
}).catch((error) => {
console.error('Error:', error);
});
}, []);
return (
<div>
<h3>Orders (sorted by CustomerID desc)</h3>
<ul>
{orders.map((order) => (
<li key={order.OrderID}>{order.CustomerID} - ${order.Freight}</li>
))}
</ul>
<p>Note: Sorting state persisted. Refresh page to see state restored.</p>
</div>
);
}Excluding Queries from Persistence
import React, { useEffect, useState } from 'react';
import { DataManager, UrlAdaptor, Query } from '@syncfusion/ej2-data';
export default function SelectivePersistenceExample() {
const [products, setProducts] = useState([]);
useEffect(() => {
const dataManager = new DataManager({
url: 'url',
adaptor: new UrlAdaptor(),
enablePersistence: true,
id: 'reactProductManager',
ignoreOnPersist: ['onSortBy', 'onSearch'] // Don't persist sorting and search
});
// These will be persisted: filtering, grouping
// These will NOT be persisted: sorting, search
dataManager.executeQuery(
new Query()
.where('Status', 'equal', 'Active')
.sortBy('Price', 'descending') // Won't persist
).then((result) => {
setProducts(result.result);
}).catch((error) => {
console.error('Error:', error);
});
}, []);
return (
<div>
<h3>Products</h3>
<ul>
{products.map((product) => (
<li key={product.ProductID}>{product.ProductName}</li>
))}
</ul>
</div>
);
}Retrieve Persisted State
import React from 'react';
import { DataManager } from '@syncfusion/ej2-data';
export default function GetPersistedDataExample() {
const handleGetState = () => {
// Retrieve the persisted query state from localStorage
const persisted = DataManager.getPersistedData('reactOrderManager');
console.log('Persisted state:', persisted);
// Output: { onSortBy: [{field: 'CustomerID', direction: 'descending'}], ... }
};
return (
<button onClick={handleGetState}>Show Persisted State</button>
);
}Update Persisted State
import React from 'react';
import { DataManager, Query } from '@syncfusion/ej2-data';
export default function SetPersistedDataExample() {
const handleUpdateState = () => {
// Create a new query to be persisted
const newQuery = new Query()
.where('Freight', 'greaterThan', 100)
.sortBy('EmployeeID', 'descending');
// Update the persisted state in localStorage
DataManager.setPersistData(
null, // event: null when not in event context
'reactOrderManager', // DataManager id
newQuery // New query to persist
);
console.log('Persisted state updated');
};
return (
<button onClick={handleUpdateState}>Update Persisted State</button>
);
}Clear Persisted State
import React from 'react';
import { DataManager } from '@syncfusion/ej2-data';
export default function ClearPersistenceExample() {
const handleClear = () => {
// Remove all persisted data for this DataManager
DataManager.clearPersistence('reactOrderManager');
console.log('Persisted state cleared - DataManager reset to initial state');
};
const handleLogout = () => {
// Clear persistence for multiple DataManagers on logout
DataManager.clearPersistence('reactOrderManager');
DataManager.clearPersistence('reactProductManager');
console.log('All persisted states cleared');
};
return (
<div>
<button onClick={handleClear}>Clear This DataManager</button>
<button onClick={handleLogout}>Clear All (Logout)</button>
</div>
);
}Persistence Properties Reference
| Property | Type | Purpose |
|---|---|---|
enablePersistence | boolean | Enable/disable state persistence |
id | string | Unique identifier for localStorage key |
ignoreOnPersist | string[] | Query types to exclude: onSortBy, onSearch, onWhere, Grouping |
---
Memory Management
Prevent memory leaks and manage memory efficiently:
🔒 Security Warning:localStorageandsessionStoragestore data unencrypted in the browser. Never store sensitive data (passwords, tokens, PII, payment info, user secrets, authentication credentials) in persisted TreeGrid state. State persistence is safe for UI state only (expand/collapse state, page number, sort order, column visibility, filter selections). For sensitive configuration or user data, use secure server-side session storage instead.
import { DataManager, WebApiAdaptor, Query } from '@syncfusion/ej2-data';
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor(),
enableCache: true
});
// ❌ WRONG: Creates memory leak
const loadAllPagesBad = () => {
for (let i = 0; i < 1000; i++) {
dataManager.executeQuery(
new Query().take(10).skip(i * 10)
); // All promises kept in memory - results never freed
}
};
// ✅ CORRECT: Load on demand
const loadPageOnDemand = (pageNumber: number) => {
dataManager.executeQuery(
new Query()
.take(10)
.skip((pageNumber - 1) * 10)
).then((e: any) => {
// Process page, release previous results
console.log('Page loaded:', e.result);
// e.result is automatically garbage collected after use
}).catch((error: any) => {
console.error('Load failed:', error);
});
};
// ✅ CORRECT: Clear cache periodically
setInterval(() => {
// Clear old cache entries manually if needed
localStorage.removeItem('DataManager_old_cache');
// Or clear specific datetime-based caches
}, 3600000); // Every hour
// ✅ CORRECT: Limit concurrent requests
let activeRequests = 0;
const maxConcurrent = 5;
const loadWithLimit = async (pageNumber: number) => {
if (activeRequests >= maxConcurrent) {
// Queue or wait
await new Promise(r => setTimeout(r, 100));
}
activeRequests++;
try {
const result = await dataManager.executeQuery(
new Query().take(10).skip((pageNumber - 1) * 10)
);
return result.result;
} finally {
activeRequests--;
}
};Memory management best practices:
- Use
take()andskip()for pagination, not load-all patterns - Enable
enableCache: truebut understand cache size limits - Clear localStorage cache periodically for offline-enabled DataManagers
- Limit concurrent requests to avoid browser resource exhaustion
- Release references to large result sets when no longer needed
---
Error Handling Strategies
DataManager uses Promise-based error handling. Errors from executeQuery() and executeLocal() are caught using .catch() on the Promise or try/catch with async/await.
Strategy 1: Promise .catch() Pattern
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor(),
keyColumn: 'OrderID'
});
const query = new Query().take(10);
dataManager.executeQuery(query)
.then((result) => {
console.log('Data loaded successfully:', result);
})
.catch((error) => {
// Error object properties: status, statusText, message
if (error.status === 401) {
console.error('Unauthorized: Please log in again');
} else if (error.status === 500) {
console.error('Server error: Please try again later');
} else if (error.status === 0) {
console.error('Network error: Check your connection');
} else {
console.error('Failed to load data:', error.message);
}
});Strategy 2: Try/Catch with Async/Await
const handleDataLoad = async () => {
try {
const query = new Query().take(10);
const result = await dataManager.executeQuery(query);
console.log('Data loaded successfully:', result);
} catch (error) {
if (error.status === 401) {
// Handle authentication errors
redirectToLogin();
} else if (error.status === 403) {
// Handle authorization errors
showMessage('You do not have permission to access this data');
} else if (error.status === 500) {
// Handle server errors
showMessage('Server error - please try again later');
} else if (error.status === 0) {
// Handle network errors
showMessage('Network error - check your connection');
} else {
// Handle other errors
showMessage('Failed to load data');
}
}
};Strategy 3: Retry with Exponential Backoff
const executeWithRetry = async (query: Query, maxRetries = 3): Promise<any> => {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await dataManager.executeQuery(query);
} catch (error) {
if (attempt === maxRetries) {
throw error; // Throw on final attempt
}
// Exponential backoff: 1s, 2s, 4s
const delayMs = Math.pow(2, attempt - 1) * 1000;
await new Promise(resolve => setTimeout(resolve, delayMs));
}
}
};
// Usage
try {
const result = await executeWithRetry(query);
} catch (error) {
console.error('Failed after retries:', error);
}Strategy 4: Fallback Data with User Notification
const loadDataWithFallback = async (query: Query) => {
try {
return await dataManager.executeQuery(query);
} catch (error) {
console.warn('Failed to load from server, using fallback data:', error);
// Show user notification
showNotification({
type: 'warning',
message: 'Using offline data - some information may be outdated'
});
// Return cached or default data
return getFallbackData();
}
};Key Points on Error Handling:
- Error objects contain:
status(HTTP status code),statusText,message,responseText - Common status codes: 401 (unauthorized), 403 (forbidden), 500 (server error), 0 (network error)
- Always catch errors from
executeQuery()andexecuteLocal()using.catch()ortry/catch - Use error status codes to determine appropriate user-facing messages
- Apply retry logic for transient failures (network errors, temporary server issues)
- Implement fallback strategies for mission-critical data operations
---
Performance Optimization Tips
| Tip | Impact | Implementation |
|---|---|---|
| Use pagination | High | take()/skip() for large datasets |
| Enable caching | High | enableCache: true |
| Select only needed fields | Medium | select(['field1', 'field2']) |
| Use appropriate adaptor | High | Match your backend (OData, WebApi, etc.) |
| Async/await instead of callbacks | Low | Cleaner code, same performance |
| Virtual scrolling | High | For 10,000+ visible rows |
| Expand over multiple queries | Medium | Use expand() for related data |
| Batch CRUD operations | High | saveChanges() instead of multiple calls |
---
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
| No KeyField | Can't update/delete | Set keyColumn property |
| Large dataset without paging | Browser freeze | Implement pagination |
| Cache too aggressive | Stale data | Understand cache clearing rules |
| Missing error handling | Silent failures | Add try/catch with async/await or .catch() on Promises |
| Synchronous CRUD | Blocking UI | Use async/await patterns |
| No offline support | Offline not working | Set offline: true |
| All data on first load | Slow initial load | Use pagination/lazy loading |
| Unoptimized queries | Slow performance | Use select(), where(), pagination |
---
Complete Advanced Example
import React, { useEffect, useState, useCallback } from 'react';
import { DataManager, ODataV4Adaptor, Query } from '@syncfusion/ej2-data';
export default function AdvancedDataManager() {
const [orders, setOrders] = useState([]);
const [page, setPage] = useState(1);
const [filter, setFilter] = useState('');
const [sort, setSortBy] = useState('OrderDate');
const [loading, setLoading] = useState(false);
const dataManager = new DataManager({
url: 'url',
adaptor: new ODataV4Adaptor(),
enableCache: true,
offline: true,
keyColumn: 'OrderID'
});
const loadData = useCallback(async () => {
setLoading(true);
try {
const pageSize = 20;
let query = new Query()
.skip((page - 1) * pageSize)
.take(pageSize)
.expand('Customer')
.sortBy(sort);
if (filter) {
query = query.where('ShipCity', 'equal', filter);
}
const result = await dataManager.executeQuery(query);
setOrders(result.result);
} catch (error) {
console.error('Load failed:', error);
} finally {
setLoading(false);
}
}, [page, filter, sort]);
useEffect(() => {
loadData();
}, [loadData]);
return (
<div>
<input
type="text"
placeholder="Filter by city..."
value={filter}
onChange={(e) => {
setFilter(e.target.value);
setPage(1); // Reset to page 1 on filter change
}}
/>
<select value={sort} onChange={(e) => setSortBy(e.target.value)}>
<option value="OrderDate">Date</option>
<option value="ShipCity">City</option>
<option value="Freight">Freight</option>
</select>
{loading && <p>Loading...</p>}
<table>
<thead>
<tr>
<th>Order ID</th>
<th>Customer</th>
<th>City</th>
<th>Date</th>
</tr>
</thead>
<tbody>
{orders.map((order) => (
<tr key={order.OrderID}>
<td>{order.OrderID}</td>
<td>{order.Customer?.CompanyName}</td>
<td>{order.ShipCity}</td>
<td>{new Date(order.OrderDate).toLocaleDateString()}</td>
</tr>
))}
</tbody>
</table>
<nav>
<button onClick={() => setPage(p => Math.max(1, p - 1))} disabled={page === 1}>
Previous
</button>
<span>Page {page}</span>
<button onClick={() => setPage(p => p + 1)}>Next</button>
</nav>
</div>
);
}Caching & Offline Mode in React DataManager
Table of Contents
- enableCache Property
- How Cache Works
- Cache Clearing
- Programmatic Cache Clearing
- Offline Mode Implementation
- Local Storage Persistence
- Sync After Reconnection
- Performance Best Practices
---
enableCache Property
Enable caching to prevent redundant HTTP requests for previously visited pages:
import React, { useEffect, useState } from 'react';
import { DataManager, WebApiAdaptor, Query } from '@syncfusion/ej2-data';
export default function CachingExample() {
const [page1Data, setPage1Data] = useState([]);
const [page2Data, setPage2Data] = useState([]);
useEffect(() => {
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor(),
enableCache: true // Enable caching
});
// First request - hits server
dataManager.executeQuery(new Query().take(10).skip(0))
.then((e) => {
console.log('Page 1 loaded from server');
setPage1Data(e.result);
});
// Later request - same page retrieved from cache
dataManager.executeQuery(new Query().take(10).skip(0))
.then((e) => {
console.log('Page 1 loaded from cache (no server call)');
setPage1Data(e.result);
});
// Different page - hits server
dataManager.executeQuery(new Query().take(10).skip(10))
.then((e) => {
console.log('Page 2 loaded from server');
setPage2Data(e.result);
});
}, []);
return <div>Check network tab to see cached requests</div>;
}Benefits:
- Reduces server load
- Faster UI response
- Lower bandwidth usage
- Better offline support
- Seamless pagination
Performance impact:
- First load: No improvement (still hits server)
- Subsequent same-page loads: ~95% faster (from cache)
- Memory usage: Increases slightly (stores pages in RAM)
---
How Cache Works
DataManager automatically generates a unique cache ID and stores page data:
// Without caching
const dm1 = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor(),
enableCache: false // Each request hits server
});
// Query page 1
dm1.executeQuery(new Query().take(10).skip(0)) // Server request
.then((e) => setData(e.result));
// Query page 1 again
dm1.executeQuery(new Query().take(10).skip(0)) // Another server request (same data!)
// With caching
const dm2 = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor(),
enableCache: true // Pages cached in memory
});
// Query page 1
dm2.executeQuery(new Query().take(10).skip(0)) // Server request, then cached
.then((e) => setData(e.result));
// Query page 1 again
dm2.executeQuery(new Query().take(10).skip(0)) // Retrieved from cache (no server call!)
.then((e) => setData(e.result));
// Query page 2
dm2.executeQuery(new Query().take(10).skip(10)) // Server request (new page)
.then((e) => setData(e.result));
// Query page 1 once more
dm2.executeQuery(new Query().take(10).skip(0)) // From cache again
.then((e) => setData(e.result));---
Cache Clearing
Cache is automatically cleared when data operations occur:
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor(),
enableCache: true
});
// Load page 1 (cached)
dataManager.executeQuery(new Query().take(10)).then((e) => {
console.log('Page 1 cached');
});
// Cache is cleared when:
await dataManager.insert(newOrder, 'Orders'); // Insert clears cache
await dataManager.update('OrderID', order, 'Orders'); // Update clears cache
await dataManager.remove('OrderID', id, 'Orders'); // Delete clears cache
// After any CRUD operation
dataManager.executeQuery(new Query().take(10)).then((e) => {
console.log('Page 1 reloaded from server (cache was cleared)');
});Cache cleared on:
- ✓ insert() - add new records
- ✓ update() - modify records
- ✓ remove() - delete records
- ✓ saveChanges() - batch operations
- ✓ where() - filtering changes
- ✓ sortBy() - sorting changes
- ✓ group() - grouping changes
Cache persists:
- ✗ Pagination (take/skip)
- ✗ Field selection (select)
- ✗ Expand (same query structure)
---
Manual Cache Clearing with clearCache()
Clear all cached data manually using the clearCache() method:
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor(),
enableCache: true
});
// Load data (cached automatically)
dataManager.executeQuery(new Query().take(10)).then(() => {
console.log('Data cached');
});
// Option 1: Clear all cached data
dataManager.clearCache();
console.log('All cache cleared');
// Next query will hit server
dataManager.executeQuery(new Query().take(10)).then(() => {
console.log('Fresh data from server');
});Method signature: dm.clearCache()
Purpose: Clears all cached data from offline mode or browser memory storage
When to use:
- After modifying offline data
- During user logout/session end
- Resetting application state
- Manually invalidating entire cache
- After critical data updates
Real-world example:
export default function OrderManagement() {
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor(),
enableCache: true
});
const handleLogout = () => {
// Clear all cached user data before logout
dataManager.clearCache();
console.log('Cache cleared for security');
// Redirect to login
window.location.href = '/login';
};
const handleCriticalUpdate = async () => {
try {
// Make critical change to data
await dataManager.update('Orders', criticalData);
// Forces all users to refresh data
dataManager.clearCache();
console.log('Cache cleared - all data refreshed');
// Notify UI to reload
window.location.reload();
} catch (error) {
console.error('Update failed:', error);
}
};
return (
<div>
<button onClick={handleLogout}>Logout</button>
<button onClick={handleCriticalUpdate}>Apply Critical Update</button>
</div>
);
}Comparison with automatic clearing:
// AUTOMATIC: CRUD operations clear cache
await dataManager.insert(newOrder, 'Orders'); // Cache cleared automatically
await dataManager.update('OrderID', updated, 'Orders'); // Cache cleared automatically
await dataManager.remove('OrderID', id, 'Orders'); // Cache cleared automatically
// MANUAL: Explicit clearCache() call
dataManager.clearCache(); // Clears all cache regardless of CRUD ops---
Offline Mode Implementation
Enable offline support for working without internet connection:
import React, { useEffect, useState } from 'react';
import { DataManager, WebApiAdaptor, Query } from '@syncfusion/ej2-data';
export default function OfflineModeExample() {
const [orders, setOrders] = useState([]);
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor(),
offline: true, // Enable offline support
enableCache: true // Cache data locally
});
// Listen for online/offline events
window.addEventListener('online', () => {
console.log('Back online - syncing data...');
setIsOnline(true);
// Trigger sync of pending changes
dataManager.saveChanges(changes).then(() => {
console.log('Data synced');
});
});
window.addEventListener('offline', () => {
console.log('Going offline - using cached data');
setIsOnline(false);
});
// Load initial data
dataManager.executeQuery(new Query().take(50))
.then((e) => {
setOrders(e.result);
console.log('Data cached locally for offline use');
});
return () => {
window.removeEventListener('online', () => {});
window.removeEventListener('offline', () => {});
};
}, []);
return (
<div>
<p>Status: {isOnline ? '🟢 Online' : '🔴 Offline'}</p>
<p>Orders: {orders.length}</p>
<p>Note: Changes made while offline will sync when reconnected</p>
</div>
);
}Offline mode workflow: 1. User loads data while online 2. Data cached in browser (localStorage) 3. User goes offline, makes changes 4. Changes queued locally 5. User returns online 6. Changes automatically synced to server
---
Local Storage Persistence
Store cached data in browser's localStorage for persistent offline access:
🔒 Security Warning:localStorageandsessionStoragestore data unencrypted in the browser. Never store sensitive data (passwords, tokens, PII, payment info, user secrets, authentication credentials) in persisted TreeGrid state. State persistence is safe for UI state only (expand/collapse state, page number, sort order, column visibility, filter selections). For sensitive configuration or user data, use secure server-side session storage instead.
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor(),
offline: true // Uses localStorage by default
});
// Data automatically saved to localStorage
dataManager.executeQuery(new Query().take(100)).then((e) => {
// Data also stored in:
// localStorage.key = "DataManager_Orders_0" containing e.result
});
// Even after browser closes/reopens, data is available
// (as long as localStorage isn't cleared)
window.addEventListener('beforeunload', () => {
// localStorage automatically updated before page unload
console.log('Data persisted to localStorage');
});localStorage details:
- Capacity: 5-10 MB per domain (varies by browser)
- Persistence: Survives browser close/restart
- Security: Same-origin policy (same domain only)
- Clearing: User can clear via browser settings
Check what's stored:
// View all DataManager cache in browser DevTools
Object.keys(localStorage).forEach(key => {
if (key.startsWith('DataManager')) {
console.log(key, localStorage.getItem(key));
}
});
// Clear cache manually
localStorage.removeItem('DataManager_Orders_0');
localStorage.clear(); // Clear all---
Sync After Reconnection
Handle synchronization of offline changes when reconnecting:
import React, { useEffect, useState } from 'react';
import { DataManager, WebApiAdaptor, Query } from '@syncfusion/ej2-data';
export default function OfflineSyncExample() {
const [pendingChanges, setPendingChanges] = useState({
added: [],
changed: [],
deleted: []
});
useEffect(() => {
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor(),
offline: true,
keyColumn: 'OrderID'
});
let offlineChanges = {
added: [],
changed: [],
deleted: []
};
// Track changes while offline
const queueChange = (type, data) => {
offlineChanges[type].push(data);
setPendingChanges(offlineChanges);
};
// Simulate offline edit
const editOrder = (order) => {
if (navigator.onLine) {
// Online - sync immediately
dataManager.update('Orders', order);
} else {
// Offline - queue for later
queueChange('changed', order);
}
};
// Listen for reconnection
window.addEventListener('online', async () => {
if (offlineChanges.added.length > 0 ||
offlineChanges.changed.length > 0 ||
offlineChanges.deleted.length > 0) {
console.log('Syncing offline changes...');
try {
const result = await dataManager.saveChanges(offlineChanges);
console.log('Sync complete:', result);
// Clear queued changes after successful sync
offlineChanges = { added: [], changed: [], deleted: [] };
setPendingChanges(offlineChanges);
} catch (error) {
console.error('Sync failed:', error);
// Keep pending changes for retry
}
}
});
return () => {
window.removeEventListener('online', () => {});
};
}, []);
return (
<div>
<p>Pending changes: {pendingChanges.added.length +
pendingChanges.changed.length +
pendingChanges.deleted.length}</p>
</div>
);
}Sync patterns: 1. Immediate sync: Push changes on reconnect 2. Conflict resolution: Handle data conflicts 3. Partial sync: Retry failed items 4. Timeout handling: Mark old offline data as stale
---
Performance Best Practices
1. Use Pagination with Caching
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor(),
enableCache: true,
pageSize: 20 // Smaller pages = more efficient cache
});
// Load pages as needed
const loadPage = (pageNumber) => {
const pageSize = 20;
dataManager.executeQuery(
new Query()
.take(pageSize)
.skip((pageNumber - 1) * pageSize)
).then((e) => {
// Each page cached separately
console.log(`Page ${pageNumber} loaded`);
});
};2. Monitor Cache Size
// Large datasets + caching = high memory usage
// Solution: Limit cache or use virtual scrolling
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor(),
enableCache: true,
// Don't cache if expecting 10,000+ records
pageSize: 50 // Smaller pages, load on demand
});3. Clear Cache Explicitly
// Force refresh
const refreshData = async () => {
// Trigger a dummy CRUD to clear cache
await dataManager.insert(emptyRecord, 'Orders');
// Then reload
dataManager.executeQuery(new Query().take(10))
.then((e) => setData(e.result));
};4. Combine Cache + Offline Mode
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor(),
enableCache: true, // Cache for performance
offline: true, // Also persist for offline
crossDomain: true
});
// Result: Fast loading + Offline support + Automatic sync---
Limitations & Gotchas
| Feature | Limitation |
|---|---|
| Cache size | Limited to available browser RAM (varies by browser) |
| Large datasets | Cache needs separate memory - consider pagination |
| Real-time data | Cache may be stale - no live updates |
| Multi-tab sync | Cache not shared between browser tabs |
| Mobile | Limited storage on mobile devices |
| Security | Cached data visible via DevTools (don't cache secrets) |
---
Complete Example: Cache + Offline + Sync
import React, { useEffect, useState } from 'react';
import { DataManager, WebApiAdaptor, Query } from '@syncfusion/ej2-data';
export default function CompleteOfflineExample() {
const [orders, setOrders] = useState([]);
const [isOnline, setIsOnline] = useState(navigator.onLine);
const [pendingCount, setPendingCount] = useState(0);
useEffect(() => {
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor(),
enableCache: true, // Performance optimization
offline: true, // Offline support
keyColumn: 'OrderID'
});
let changes = { added: [], changed: [], deleted: [] };
// Online/offline listeners
window.addEventListener('online', () => {
setIsOnline(true);
// Sync any pending changes
if (changes.added.length > 0 || changes.changed.length > 0 || changes.deleted.length > 0) {
dataManager.saveChanges(changes).then(() => {
changes = { added: [], changed: [], deleted: [] };
setPendingCount(0);
});
}
});
window.addEventListener('offline', () => {
setIsOnline(false);
});
// Load data
dataManager.executeQuery(new Query().take(50))
.then((e) => setOrders(e.result));
}, []);
return (
<div>
<header>
<p>Status: {isOnline ? '🟢 Online' : '🔴 Offline'}</p>
{pendingCount > 0 && <p>Pending changes: {pendingCount}</p>}
</header>
<table>
<thead><tr><th>Order</th><th>Customer</th></tr></thead>
<tbody>
{orders.map((order) => (
<tr key={order.OrderID}>
<td>{order.OrderID}</td>
<td>{order.CustomerID}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}CRUD Operations in React DataManager
Table of Contents
- Insert - Adding Records
- Update - Modifying Records
- Remove - Deleting Records
- Batch Operations with saveChanges()
- Validation Before CRUD
- Transaction Patterns
---
Insert - Adding Records
Add new records to the data source using the insert() method:
import React, { useState } from 'react';
import { DataManager, WebApiAdaptor } from '@syncfusion/ej2-data';
export default function InsertRecord() {
const [newOrder, setNewOrder] = useState({
CustomerID: '',
Freight: 0,
OrderDate: new Date()
});
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
const handleInsert = async () => {
const insertedData = await dataManager.insert(newOrder, 'Orders');
console.log('Record inserted:', insertedData);
// For remote: Returns created record with ID
// For local: Adds to array
};
return (
<div>
<input
type="text"
placeholder="Customer ID"
onChange={(e) => setNewOrder({ ...newOrder, CustomerID: e.target.value })}
/>
<input
type="number"
placeholder="Freight"
onChange={(e) => setNewOrder({ ...newOrder, Freight: parseFloat(e.target.value) })}
/>
<button onClick={handleInsert}>Add Order</button>
</div>
);
}Important: For remote data sources with identity primary keys, the server should return the created record with the new ID in the response.
// Server response example:
{
OrderID: 10252, // Auto-generated by server
CustomerID: 'VINET',
Freight: 45.50,
OrderDate: '1996-07-10'
}---
Update - Modifying Records
Modify existing records using the update() method. Requires KeyField (primary key) to identify which record to update:
import React, { useState } from 'react';
import { DataManager, WebApiAdaptor } from '@syncfusion/ej2-data';
export default function UpdateRecord() {
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
const [editingOrder, setEditingOrder] = useState({
OrderID: 10248,
CustomerID: 'VINET',
Freight: 50.00,
OrderDate: new Date()
});
const handleUpdate = async () => {
const updatedData = await dataManager.update(
'OrderID', // keyField (primary key name)
editingOrder, // data object with updated values
'Orders', // tableName
null // optional query
);
console.log('Record updated:', updatedData);
};
return (
<div>
<input
type="number"
value={editingOrder.Freight}
onChange={(e) => setEditingOrder({
...editingOrder,
Freight: parseFloat(e.target.value)
})}
/>
<button onClick={handleUpdate}>Update Freight</button>
</div>
);
}KeyField usage:
- KeyField identifies the primary key field name in CRUD method calls
- Pass as the first parameter to update() and remove() methods
- All update/delete operations require the key field name and value
// Correct: KeyField passed as first parameter
const dm = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
await dm.update('OrderID', recordData, 'Orders');
// First param: keyField name ('OrderID')
// Second param: data to update
// Third param: table name
await dm.remove('OrderID', 10248, 'Orders');
// First param: keyField name ('OrderID')
// Second param: key value (10248)
// Third param: table name---
Remove - Deleting Records
Delete records using the remove() method. Requires KeyField to identify which record to delete:
import React, { useState } from 'react';
import { DataManager, WebApiAdaptor } from '@syncfusion/ej2-data';
export default function DeleteRecord() {
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
const [orderIdToDelete, setOrderIdToDelete] = useState(10248);
const handleDelete = async () => {
try {
await dataManager.remove(
'OrderID', // keyField (primary key name)
orderIdToDelete, // keyValue (the actual ID to delete)
'Orders' // tableName
);
console.log(`Order ${orderIdToDelete} deleted successfully`);
} catch (error) {
console.error('Delete failed:', error);
}
};
return (
<div>
<input
type="number"
value={orderIdToDelete}
onChange={(e) => setOrderIdToDelete(parseInt(e.target.value))}
placeholder="Order ID to delete"
/>
<button onClick={handleDelete}>Delete Order</button>
</div>
);
}Delete syntax:
await dataManager.remove(keyField, keyValue, tableName, query?);
// (primary key column name)
// keyValue: 10248 (the specific ID to delete)
// tableName: 'Orders'---
Batch Operations with saveChanges()
Process multiple CRUD operations in a single server request:
import React, { useState } from 'react';
import { DataManager, WebApiAdaptor } from '@syncfusion/ej2-data';
export default function BatchOperations() {
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
const [changes, setChanges] = useState({
changed: [],
added: [],
deleted: []
});
// Simulate user actions
const handleAddRecord = () => {
const newRecord = { CustomerID: 'NEWCUST', Freight: 100.50 };
changes.added.push(newRecord);
};
const handleModifyRecord = () => {
const modified = { OrderID: 10248, Freight: 75.00 };
changes.changed.push(modified);
};
const handleDeleteRecord = () => {
changes.deleted.push({ OrderID: 10249 });
};
const handleSaveChanges = async () => {
try {
// Send all changes in one request with keyField
const result = await dataManager.saveChanges(changes, 'Orders', 'OrderID');
console.log('Batch save successful:', result);
// UI updates with response
} catch (error) {
console.error('Batch save failed:', error);
}
};
return (
<div>
<button onClick={handleAddRecord}>Queue Add</button>
<button onClick={handleModifyRecord}>Queue Modify</button>
<button onClick={handleDeleteRecord}>Queue Delete</button>
<button onClick={handleSaveChanges}>Save All Changes</button>
<pre>{JSON.stringify(changes, null, 2)}</pre>
</div>
);
}Batch structure:
{
added: [
{ CustomerID: 'NEWCUST', Freight: 100 }
],
changed: [
{ OrderID: 10248, Freight: 50 }
],
deleted: [
{ OrderID: 10249 }
]
}Advantages:
- Single network request instead of 3+
- Reduced server load
- Atomic transactions (all succeed or all fail)
- Better performance for bulk operations
---
Validation Before CRUD
Validate data before sending to server:
const validateOrder = (order) => {
if (!order.CustomerID || order.CustomerID.trim() === '') {
throw new Error('Customer ID is required');
}
if (order.Freight < 0) {
throw new Error('Freight cannot be negative');
}
if (!order.OrderDate) {
throw new Error('Order Date is required');
}
return true;
};
const handleInsertWithValidation = async (newOrder) => {
try {
validateOrder(newOrder); // Throws if invalid
const result = await dataManager.insert(newOrder, 'Orders');
console.log('Insert successful:', result);
} catch (validationError) {
console.error('Validation failed:', validationError.message);
// Show error to user
} catch (apiError) {
console.error('API error:', apiError);
}
};---
Transaction Patterns
Pattern 1: Conditional CRUD
// Only insert if conditions met
const insertIfValid = async (newOrder) => {
// Check if customer exists first
const customerExists = await dataManager
.executeQuery(new Query().where('CustomerID', 'equal', newOrder.CustomerID))
.then(result => result.count > 0);
if (customerExists) {
await dataManager.insert(newOrder, 'Orders');
} else {
console.log('Customer not found - insert customer first');
}
};Pattern 2: Cascade Updates
// Update parent, then children
const updateOrderAndDetails = async (orderId, newFreight) => {
// Update order
await dataManager.update('Orders', { Freight: newFreight }, { key: orderId });
// Update related order details
const details = await dataManager
.executeQuery(new Query().where('OrderID', 'equal', orderId));
for (const detail of details.result) {
detail.Amount = detail.Quantity * newFreight;
await dataManager.update('OrderDetails', detail, { key: detail.OrderDetailID });
}
};Pattern 3: Rollback on Error
const safeInsertWithRollback = async (ordersToInsert) => {
const inserted = [];
try {
for (const order of ordersToInsert) {
const result = await dataManager.insert(order, 'Orders');
inserted.push(result);
}
} catch (error) {
// Rollback: Delete inserted records
for (const order of inserted) {
await dataManager.remove('OrderID', order.OrderID, 'Orders');
}
throw error;
}
};---
Common CRUD Errors
| Error | Cause | Solution |
|---|---|---|
| "Update without keyField" | Update call missing keyField parameter | Pass keyField as first parameter: dataManager.update('OrderID', data, 'Orders') |
| "Record not found" | KeyValue doesn't exist | Verify key value exists before update/delete |
| "Primary key duplicate" | Inserting duplicate key | Check for existing record first with where() |
| "Invalid response format" | Server returns wrong format | Verify adaptor matches server response structure |
| "CORS error" | Cross-domain request | Add crossDomain: true to DataManager initialization |
Data Binding in React DataManager
Connect your React components to data sources using DataManager. It supports both local JavaScript arrays and remote REST/OData services.
Local Data Binding
Bind DataManager directly to a JavaScript array for client-side operations:
Using the json Property
import React, { useEffect, useState } from 'react';
import { DataManager, Query } from '@syncfusion/ej2-data';
export default function LocalDataBinding() {
const [orders, setOrders] = useState([]);
useEffect(() => {
// Local data array
const dataArray = [
{ OrderID: 10248, CustomerID: 'VINET', Freight: 32.38, OrderDate: new Date(1996, 6, 4) },
{ OrderID: 10249, CustomerID: 'TOMSP', Freight: 11.61, OrderDate: new Date(1996, 6, 5) },
{ OrderID: 10250, CustomerID: 'HANAR', Freight: 65.83, OrderDate: new Date(1996, 6, 8) },
{ OrderID: 10251, CustomerID: 'VICTE', Freight: 41.34, OrderDate: new Date(1996, 6, 9) },
];
// Initialize DataManager with local data
const dataManager = new DataManager(dataArray);
// Execute local query
dataManager.executeLocal(new Query().take(10)).then((e) => {
setOrders(e.result);
});
}, []);
return (
<table>
<thead>
<tr>
<th>Order ID</th>
<th>Customer</th>
<th>Freight</th>
</tr>
</thead>
<tbody>
{orders.map((order) => (
<tr key={order.OrderID}>
<td>{order.OrderID}</td>
<td>{order.CustomerID}</td>
<td>${order.Freight.toFixed(2)}</td>
</tr>
))}
</tbody>
</table>;
);
}Passing Data During Constructor
const local_data = [
{ ID: 1, Name: 'Alice', Status: 'Active' },
{ ID: 2, Name: 'Bob', Status: 'Inactive' },
];
const dataManager = new DataManager(local_data);When to use local binding:
- Small datasets (< 1000 records)
- No server-side filtering needed
- Offline scenarios
- Quick prototyping
Remote Data Binding
Connect to remote REST APIs, OData services, or any HTTP endpoint:
Using the url Property with WebApiAdaptor
import React, { useEffect, useState } from 'react';
import { DataManager, Query, WebApiAdaptor } from '@syncfusion/ej2-data';
export default function RemoteDataBinding() {
const [orders, setOrders] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Create DataManager with remote endpoint
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor(),
crossDomain: true
});
// Execute remote query
dataManager.executeQuery(new Query().take(10)).then((e) => {
setOrders(e.result);
setLoading(false);
}).catch((error) => {
console.error('Error fetching data:', error);
setLoading(false);
});
}, []);
if (loading) return <p>Loading...</p>;
return (
<table>
<thead>
<tr>
<th>Order ID</th>
<th>Customer</th>
<th>Freight</th>
</tr>
</thead>
<tbody>
{orders.map((order) => (
<tr key={order.OrderID}>
<td>{order.OrderID}</td>
<td>{order.CustomerID}</td>
<td>${order.Freight}</td>
</tr>
))}
</tbody>
</table>
);
}When to use remote binding:
- Large datasets (server-side paging)
- Real-time data from API
- Shared data across users
- Server-side filtering/sorting needed
executeLocal() vs executeQuery()
| Method | Use Case | Data Source | Execution |
|---|---|---|---|
| executeLocal() | Client-side operations | Local arrays (json property) | In-memory, instant |
| executeQuery() | Server operations | Remote endpoints (url property) | HTTP request, async |
executeLocal() Example
// Use for local arrays - synchronous operations on client side
const dataManager = new DataManager([
{ id: 1, name: 'Product A', price: 100 },
{ id: 2, name: 'Product B', price: 200 },
]);
dataManager.executeLocal(
new Query()
.where('price', 'greaterthan', 150)
.sortBy('name')
).then((e) => {
console.log(e.result); // Returns [{id: 2, name: 'Product B', price: 200}]
});executeQuery() Example
// Use for remote data - asynchronous HTTP request
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
dataManager.executeQuery(
new Query().take(10).skip(0)
).then((e) => {
console.log(e.result); // Returns server response
}).catch((error) => {
console.error('API Error:', error);
});Switching Between Data Sources
Change data source dynamically without recreating components:
import React, { useState } from 'react';
import { DataManager, ODataV4Adaptor, Query } from '@syncfusion/ej2-data';
export default function SwitchingDataSources() {
const [dataSource, setDataSource] = useState('local');
const [data, setData] = useState([]);
const loadLocalData = () => {
const localArray = [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
];
const dm = new DataManager(localArray);
dm.executeLocal(new Query().take(10)).then((e) => setData(e.result));
};
const loadRemoteData = () => {
const dm = new DataManager({
url: 'url',
adaptor: new ODataV4Adaptor(),
crossDomain: true
});
dm.executeQuery(new Query().take(10)).then((e) => setData(e.result));
};
const handleSwitch = (source) => {
setDataSource(source);
if (source === 'local') loadLocalData();
if (source === 'remote') loadRemoteData();
};
return (
<div>
<button onClick={() => handleSwitch('local')}>Load Local Data</button>
<button onClick={() => handleSwitch('remote')}>Load Remote Data</button>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
}Response Format & Expectations
DataManager expects different response formats based on adaptor type:
JSON Response Format (JsonAdaptor / UrlAdaptor)
{
"result": [
{ "id": 1, "name": "Item 1" },
{ "id": 2, "name": "Item 2" }
],
"count": 100
}OData Response Format
{
"d": {
"results": [
{ "ID": 1, "Name": "Item 1" },
{ "ID": 2, "Name": "Item 2" }
],
"__count": "100"
}
}OData v4 Response Format
{
"value": [
{ "id": 1, "name": "Item 1" },
{ "id": 2, "name": "Item 2" }
],
"@odata.count": 100
}Error Handling During Binding
Handle errors gracefully during data binding using Promise .catch() or try/catch:
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
// Approach 1: Using Promise .catch()
dataManager.executeQuery(new Query().take(10))
.then((result) => {
console.log('Success:', result.result);
})
.catch((error) => {
console.error('Fetch failed:', error);
if (error.status === 401) {
console.log('Unauthorized - redirect to login');
} else if (error.status === 500) {
console.log('Server error - try again later');
} else if (error.status === 0) {
console.log('Network error - check connection');
}
});
// Approach 2: Using async/await with try/catch
async function loadData() {
try {
const result = await dataManager.executeQuery(new Query().take(10));
console.log('Success:', result.result);
} catch (error) {
console.error('Fetch failed:', error);
if (error.status === 401) {
// Redirect to login
} else if (error.status === 500) {
// Show server error message
}
}
}Performance Considerations
- enableCache: Set to
truefor remote data to avoid re-fetching same pages - take(): Use pagination instead of loading all records at once
- select(): Request only required fields from server
- Offline mode: Cache data locally for faster access and offline support
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor(),
enableCache: true, // Cache pages
offline: true, // Enable offline support
crossDomain: true
});Getting Started with React DataManager
Installation
Start by installing the Syncfusion DataManager package using npm:
npm install @syncfusion/ej2-data --saveDependencies
The DataManager requires the following peer dependencies:
|-- @syncfusion/ej2-data
|-- @syncfusion/ej2-base
|-- es6-promise (if window.Promise not available)Important: Ensure window.Promise is available in your browser environment. For older browsers, you may need to include a Promise polyfill.Basic Project Setup
Set up a React application using create-react-app (recommended):
npx create-react-app my-datamanager-app
cd my-datamanager-app
npm install @syncfusion/ej2-data --save
npm startFor TypeScript support:
npx create-react-app my-datamanager-app --template typescript
cd my-datamanager-app
npm install @syncfusion/ej2-data --save
npm startImporting DataManager
Import the necessary classes from @syncfusion/ej2-data:
import React from 'react';
import { DataManager, Query, JsonAdaptor } from '@syncfusion/ej2-data';
export default function App() {
return <div>Ready to use DataManager</div>;
}First DataManager Instance
Create a DataManager instance with local data:
import React, { useEffect, useState } from 'react';
import { DataManager, Query, JsonAdaptor } from '@syncfusion/ej2-data';
export default function FirstQuery() {
const [data, setData] = useState([]);
useEffect(() => {
// Sample data array
const dataArray = [
{ OrderID: 10248, CustomerID: 'VINET', Freight: 32.38 },
{ OrderID: 10249, CustomerID: 'TOMSP', Freight: 11.61 },
{ OrderID: 10250, CustomerID: 'HANAR', Freight: 65.83 },
];
// Initialize DataManager with local data
const dataManager = new DataManager(dataArray);
// Execute query
dataManager.executeLocal(new Query().take(2)).then((e) => {
setData(e.result); // Bind results to state
});
}, []);
return (
<div>
<h2>Orders</h2>
<ul>
{data.map((item) => (
<li key={item.OrderID}>{item.CustomerID}</li>
))}
</ul>
</div>
);
}Common Setup Errors & Fixes
Error: "DataManager is not defined"
- Solution: Ensure
@syncfusion/ej2-datais installed:npm install @syncfusion/ej2-data --save - Check import statement:
import { DataManager } from '@syncfusion/ej2-data';
Error: "Adaptor is not a function"
- Solution: Ensure adaptor is instantiated:
new WebApiAdaptor()(not justWebApiAdaptor)
Error: "Promise is not defined"
- Solution: Add Promise polyfill for older browsers, or ensure
window.Promiseexists
Error: CORS errors on remote requests
- Solution: Set
crossDomain: truein DataManager configuration
Error: "executeQuery is not a function"
- Solution: Use
executeQuery()for remote,executeLocal()for local data
Middleware & Customization in React DataManager
Table of Contents
- Pre-Request Middleware
- Post-Request Middleware
- Custom Headers
- Additional Parameters
- Response Transformation
- Error Handler
- Middleware Execution Order
---
Pre-Request Middleware
Intercept and modify requests before they're sent to the server using applyPreRequestMiddlewares():
🔒 Security Warning:localStorageandsessionStoragestore data unencrypted in the browser. Never store sensitive data (passwords, tokens, PII, payment info, user secrets, authentication credentials) in persisted TreeGrid state. State persistence is safe for UI state only (expand/collapse state, page number, sort order, column visibility, filter selections). For sensitive configuration or user data, use secure server-side session storage instead.
import React, { useEffect, useState } from 'react';
import { DataManager, WebApiAdaptor, Query } from '@syncfusion/ej2-data';
export default function PreRequestMiddleware() {
useEffect(() => {
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
// Apply pre-request middleware to add auth token
dataManager.applyPreRequestMiddlewares([
async (context) => {
// Fetch token from auth service
const token = localStorage.getItem('authToken');
if (token) {
// Add to request headers
context.request.headers = context.request.headers || {};
context.request.headers['Authorization'] = `send_token`;
}
// Can also modify query parameters or body
console.log('Request prepared:', context.request);
}
]);
dataManager.executeQuery(new Query().take(10))
.then((e) => console.log(e.result));
}, []);
return <div>Check console for middleware execution</div>;
}Common use cases:
- Authentication: Add JWT tokens or API keys
- Request tracking: Add correlation IDs
- Header normalization: Set Content-Type, Accept headers
- Request logging: Track API calls
- Request validation: Verify before sending
// Multiple middleware functions
dataManager.applyPreRequestMiddlewares([
async (context) => {
// First: Add auth token
context.request.headers['Authorization'] = `send_token`;
},
async (context) => {
// Second: Add correlation ID
context.request.headers['X-Correlation-ID'] = generateId();
},
async (context) => {
// Third: Log request
console.log('Outgoing request:', context.request);
}
]);---
Post-Request Middleware
Process responses before they're bound to components using applyPostRequestMiddlewares():
import React, { useEffect, useState } from 'react';
import { DataManager, WebApiAdaptor, Query } from '@syncfusion/ej2-data';
export default function PostRequestMiddleware() {
const [orders, setOrders] = useState([]);
useEffect(() => {
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
// Apply post-request middleware to transform response
dataManager.applyPostRequestMiddlewares([
async (context) => {
// Transform response data format
if (context.response && context.response.result) {
context.response.result = context.response.result.map(item => ({
id: item.OrderID,
customer: item.CustomerID,
amount: parseFloat(item.Freight),
date: new Date(item.OrderDate).toLocaleDateString()
}));
}
console.log('Response transformed:', context.response);
}
]);
dataManager.executeQuery(new Query().take(10))
.then((e) => {
setOrders(e.result); // Already transformed
});
}, []);
return (
<ul>
{orders.map((order) => (
<li key={order.id}>{order.customer} - ${order.amount}</li>
))}
</ul>
);
}Common use cases:
- Data transformation: Convert server field names to UI format
- Data formatting: Parse dates, format numbers
- Data filtering: Remove sensitive fields
- Data aggregation: Combine related fields
- Response validation: Verify expected structure
- Error mapping: Transform error responses
// Filter sensitive data from response
dataManager.applyPostRequestMiddlewares([
async (context) => {
context.response.result = context.response.result.map(item => {
const { password, ssn, ...safe } = item;
return safe; // Exclude sensitive fields
});
}
]);
// Format currency and dates
dataManager.applyPostRequestMiddlewares([
async (context) => {
context.response.result = context.response.result.map(item => ({
...item,
Freight: `$${item.Freight.toFixed(2)}`,
OrderDate: new Date(item.OrderDate).toLocaleDateString('en-US')
}));
}
]);---
Custom Headers
Add custom headers to all requests:
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
dataManager.applyPreRequestMiddlewares([
async (context) => {
context.request.headers = {
...context.request.headers,
'Authorization': `send_token`,
'X-API-Version': '2.0',
'X-Client-ID': 'mobile-app',
'X-Request-ID': generateUUID(),
'User-Agent': 'CustomApp/1.0'
};
}
]);Common custom headers:
Authorization: Authentication tokenX-API-Key: API key (less secure than Bearer)X-API-Version: API version (for versioning)X-Client-ID: Identify client appX-Request-ID: Correlation ID for loggingX-Tenant-ID: Multi-tenant applications
// Conditional header based on user role
dataManager.applyPreRequestMiddlewares([
async (context) => {
const userRole = getCurrentUserRole();
if (userRole === 'admin') {
context.request.headers['X-Admin-Access'] = 'true';
}
if (isOfflineMode()) {
context.request.headers['X-Offline-Mode'] = 'sync';
}
}
]);---
Additional Parameters
Send extra data with every request:
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
// Method 1: Pre-request middleware
dataManager.applyPreRequestMiddlewares([
async (context) => {
// Add query parameters
const params = new URLSearchParams();
params.append('userId', '12345');
params.append('language', 'en-US');
params.append('timezone', 'UTC');
context.request.url = context.request.url + '?' + params.toString();
}
]);
// Method 2: With Query class (server-side filtering)
const query = new Query()
.addParams('userId', '12345')
.addParams('language', 'en-US')
.take(10);
dataManager.executeQuery(query);Sending additional parameters for:
- User context: User ID, timezone, language
- Tracking: Session ID, request ID, source
- Filtering: Tenant ID, organization, etc.
- API versioning: Version number
// Multi-tenant example
dataManager.applyPreRequestMiddlewares([
async (context) => {
const tenantId = getCurrentTenantId();
// Add to URL
context.request.url = context.request.url + `?tenantId=${tenantId}`;
// Or add to headers
context.request.headers['X-Tenant-ID'] = tenantId;
}
]);---
Response Transformation
Convert server response format to application format:
// Problem: Server returns old format, app expects new format
// Server response:
// { order_id: 123, customer_name: "John", total_amount: 99.99 }
// Expected format:
// { OrderID: 123, CustomerName: "John", TotalAmount: 99.99 }
dataManager.applyPostRequestMiddlewares([
async (context) => {
context.response.result = context.response.result.map(item => ({
OrderID: item.order_id,
CustomerName: item.customer_name,
TotalAmount: item.total_amount,
FormattedAmount: `$${item.total_amount.toFixed(2)}`
}));
}
]);Advanced transformation:
// Convert nested response structure
dataManager.applyPostRequestMiddlewares([
async (context) => {
// Flatten nested data
context.response.result = context.response.result.map(item => ({
...item,
CustomerCity: item.customer?.address?.city,
CustomerCountry: item.customer?.address?.country
}));
}
]);
// Aggregate related items
dataManager.applyPostRequestMiddlewares([
async (context) => {
const grouped = {};
for (const item of context.response.result) {
const key = item.orderId;
if (!grouped[key]) {
grouped[key] = { ...item, items: [] };
}
grouped[key].items.push(item.lineItem);
}
context.response.result = Object.values(grouped);
}
]);---
Error Handling with Middleware
Handle errors from middleware-wrapped requests using Promise .catch() or try/catch:
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
// Apply pre-request middleware
dataManager.applyPreRequestMiddlewares = async (args: any) => {
const headers = args.headers || {};
headers['Authorization'] = `send_token`;
args.headers = headers;
return args;
};
// Handle errors by catching on executeQuery/executeLocal Promises
dataManager.executeQuery(new Query())
.then((result) => {
console.log('Success:', result.result);
})
.catch((error) => {
console.error('Middleware or fetch error:', error);
// Handle specific error types
if (error.status === 401) {
// Unauthorized - token may be expired
refreshToken();
redirectToLogin();
} else if (error.status === 403) {
// Forbidden - permission denied
showNotification('You do not have permission to access this data');
} else if (error.status === 500) {
// Server error
console.error('Server error - please try again later');
} else if (error.status === 0) {
// Network error
console.error('Network error - check your connection');
}
});---
Middleware Execution Order
Middleware functions execute in defined order:
1. Pre-Request Middleware (in order defined)
↓
2. HTTP Request sent
↓
3. Response received
↓
4. Post-Request Middleware (in order defined)
↓
5. Data bound to componentExample showing order:
dataManager.applyPreRequestMiddlewares([
async (context) => {
console.log('1. First pre-request middleware');
},
async (context) => {
console.log('2. Second pre-request middleware');
}
]);
dataManager.applyPostRequestMiddlewares([
async (context) => {
console.log('4. First post-request middleware');
},
async (context) => {
console.log('5. Second post-request middleware');
}
]);
dataManager.executeQuery(new Query().take(10))
.then((e) => {
console.log('6. Results bound:', e.result);
});
// Console output order:
// 1. First pre-request middleware
// 2. Second pre-request middleware
// (Request sent...)
// 4. First post-request middleware
// 5. Second post-request middleware
// 6. Results bound---
Complete Example: Authentication + Transformation + Error Handling
🔒 Security Warning:localStorageandsessionStoragestore data unencrypted in the browser. Never store sensitive data (passwords, tokens, PII, payment info, user secrets, authentication credentials) in persisted TreeGrid state. State persistence is safe for UI state only (expand/collapse state, page number, sort order, column visibility, filter selections). For sensitive configuration or user data, use secure server-side session storage instead.
import React, { useEffect, useState } from 'react';
import { DataManager, WebApiAdaptor, Query } from '@syncfusion/ej2-data';
export default function CompleteMiddlewareExample() {
const [orders, setOrders] = useState([]);
const [error, setError] = useState(null);
useEffect(() => {
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
// Pre-request: Add authentication
dataManager.applyPreRequestMiddlewares([
async (context) => {
const token = localStorage.getItem('authToken');
context.request.headers = {
...context.request.headers,
'Authorization': `send_token`,
'X-REQUEST-ID': Math.random().toString(36).substr(2, 9)
};
}
]);
// Post-request: Transform response
dataManager.applyPostRequestMiddlewares([
async (context) => {
if (context.response?.result) {
context.response.result = context.response.result.map(item => ({
id: item.OrderID,
customer: item.CustomerID,
freight: `$${parseFloat(item.Freight).toFixed(2)}`,
date: new Date(item.OrderDate).toLocaleDateString()
}));
}
}
]);
// Execute query with error handling
dataManager.executeQuery(new Query().take(10))
.then((e) => {
setOrders(e.result);
setError(null);
})
.catch((err) => {
console.error('Failed to load orders:', err);
// Handle specific errors
if (err.status === 401) {
setError('Authentication failed - please log in again');
redirectToLogin();
} else if (err.status === 403) {
setError('You do not have permission to access this data');
} else if (err.status === 0) {
setError('Network error - check your connection');
} else {
setError('Failed to load data - please try again');
}
});
}, []);
if (error) {
return <div style={{color: 'red'}}>{error}</div>;
}
return (
<table>
<thead>
<tr>
<th>ID</th>
<th>Customer</th>
<th>Freight</th>
<th>Date</th>
</tr>
</thead>
<tbody>
{orders.map((order) => (
<tr key={order.id}>
<td>{order.id}</td>
<td>{order.customer}</td>
<td>{order.freight}</td>
<td>{order.date}</td>
</tr>
))}
</tbody>
</table>
);
}Querying & Filtering in React DataManager
Table of Contents
- Query Class Fundamentals
- Specifying Resource with from()
- Filtering with where()
- Predicate Logic
- Sorting
- Pagination with take() and skip()
- Field Projection with select()
- Related Data with expand()
- Grouping Data
- Chaining Queries
- Complex Filter Scenarios
---
Query Class Fundamentals
The Query class builds structured database-like queries for both local and remote data sources. Chain methods to compose complex data operations:
import React, { useEffect, useState } from 'react';
import { DataManager, Query, JsonAdaptor } from '@syncfusion/ej2-data';
export default function QueryExample() {
const [results, setResults] = useState([]);
useEffect(() => {
const data = [
{ OrderID: 10248, CustomerID: 'VINET', Freight: 32.38, OrderDate: new Date(1996, 6, 4) },
{ OrderID: 10249, CustomerID: 'TOMSP', Freight: 11.61, OrderDate: new Date(1996, 6, 5) },
{ OrderID: 10250, CustomerID: 'HANAR', Freight: 65.83, OrderDate: new Date(1996, 6, 8) },
];
// Create query: Filter → Sort → Paginate
const query = new Query()
.where('Freight', 'greaterthan', 30)
.sortBy('OrderDate')
.take(5);
const dm = new DataManager(data);
dm.executeLocal(query).then((e) => {
setResults(e.result);
});
}, []);
return <pre>{JSON.stringify(results, null, 2)}</pre>;
}Key points:
- Query methods return the Query instance, enabling chaining
- Use
executeLocal()for local data orexecuteQuery()for remote - Results come in
e.resultfrom the Promise
---
Specifying Resource with from()
Identify which data source or table to query using from():
// For OData services with multiple entities
const query = new Query()
.from('Orders') // Entity name in OData service
.take(10);
// For remote APIs, from() is optional - url defines the resource
const dm = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
// From is primarily used with OData
const odataQuery = new Query()
.from('Products')
.where('Price', 'lessthan', 100);---
Filtering with where()
Create filter conditions to retrieve matching records:
// Simple condition
new Query().where('CustomerID', 'equal', 'VINET')
// Comparison operators
new Query()
.where('Freight', 'greaterthan', 100) // >
.where('Freight', 'lessthan', 500) // <
.where('Freight', 'greaterthanorequal', 50) // >=
.where('Freight', 'lessthanorequal', 200) // <=
.where('OrderID', 'notequal', 10248) // !=
// String operations
new Query()
.where('CustomerID', 'startswith', 'V') // Starts with
.where('CustomerID', 'endswith', 'T') // Ends with
.where('CustomerID', 'contains', 'VI') // ContainsMultiple Filters (AND Logic)
Chain where() calls for AND conditions:
const query = new Query()
.where('OrderDate', 'greaterthan', new Date(1996, 5, 1))
.where('Freight', 'lessthan', 100)
.where('CustomerID', 'equal', 'VINET');
// Returns records WHERE OrderDate > June 1, 1996 AND Freight < 100 AND CustomerID = 'VINET'---
Predicate Logic
Use Predicate class for complex OR/AND combinations:
import { Predicate } from '@syncfusion/ej2-data';
// OR condition
const predicate = new Predicate('CustomerID', 'equal', 'VINET')
.or('CustomerID', 'equal', 'TOMSP');
const query = new Query().where(predicate);
// Returns: CustomerID = 'VINET' OR CustomerID = 'TOMSP'
// Complex: (A OR B) AND C
const predicate2 = new Predicate('CustomerID', 'equal', 'VINET')
.or('CustomerID', 'equal', 'TOMSP')
.and('Freight', 'greaterthan', 50);
const query2 = new Query().where(predicate2);
// Returns: (CustomerID = 'VINET' OR CustomerID = 'TOMSP') AND Freight > 50---
Sorting
Arrange records in ascending or descending order:
// Ascending (default)
new Query().sortBy('OrderDate')
// Descending
new Query().sortByDesc('Freight')
// Multiple sorts
new Query()
.sortBy('CustomerID') // First by Customer
.sortByDesc('OrderDate'); // Then by Date descending
// Dynamic sort
new Query().sortBy('Freight', true) // true = descending---
Pagination with take() and skip()
Limit results and implement paging:
// Get first 10 records
new Query().take(10)
// Skip first 20, get next 10 (page 3, page size 10)
new Query().skip(20).take(10)
// Page calculation
const pageSize = 10;
const pageNumber = 2; // 0-based
const skip = pageNumber * pageSize; // 10
new Query().skip(skip).take(pageSize)
// For grid - Grid.pageSettings manages this, but DataManager handles raw API
const query = new Query()
.skip((currentPage - 1) * pageSize)
.take(pageSize);---
Field Projection with select()
Retrieve only specific fields, reducing payload size:
// Select specific columns
new Query().select(['OrderID', 'CustomerID', 'Freight'])
// From remote API - sends to server
const query = new Query()
.select(['OrderID', 'CustomerID', 'Freight'])
.take(100);
const dm = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
dm.executeQuery(query).then((e) => {
// e.result only has OrderID, CustomerID, Freight
console.log(e.result[0]); // { OrderID: 10248, CustomerID: 'VINET', Freight: 32.38 }
});
// Local data - still filters properties
const localDm = new DataManager(dataArray);
localDm.executeLocal(query).then((e) => {
console.log(e.result[0]); // Only selected fields returned
});---
Related Data with expand()
Eagerly load related objects instead of separate queries:
// Without expand - Customer data requires separate fetch
const query1 = new Query().take(10);
// Returns: { OrderID: 10248, CustomerID: 'VINET', Freight: 32.38 }
// With expand - Related data included
const query2 = new Query()
.expand('Customer') // Fetch Customer details from related table
.take(10);
const dm = new DataManager({
url: 'url',
adaptor: new ODataV4Adaptor()
});
dm.executeQuery(query2).then((e) => {
// Returns:
// {
// OrderID: 10248,
// CustomerID: 'VINET',
// Freight: 32.38,
// Customer: { CustomerID: 'VINET', CompanyName: 'Vins et alcools Chevalier', ... }
// }
console.log(e.result[0].Customer.CompanyName);
});
// Multiple related entities
const query3 = new Query()
.expand('Customer')
.expand('Employee')
.take(10);---
Grouping Data
Organize records by field values:
// Group by CustomerID
const query = new Query()
.group('CustomerID')
.take(10);
const dm = new DataManager(orders);
dm.executeLocal(query).then((e) => {
// Returns grouped structure:
// [
// { key: 'VINET', items: [{...}, {...}], count: 2 },
// { key: 'TOMSP', items: [{...}], count: 1 }
// ]
console.log(e.result);
});
// Group by multiple fields
const query2 = new Query()
.group('CustomerID')
.group('OrderDate');
// Group with aggregation
import { Aggregates } from '@syncfusion/ej2-data';
const query3 = new Query()
.group('CustomerID')
.aggregate('sum', 'Freight');---
Chaining Queries
Combine multiple operations in a single query:
const complexQuery = new Query()
// Filter
.where('OrderDate', 'greaterthanorequal', new Date(1996, 0, 1))
.where('Freight', 'lessthan', 500)
// Select fields
.select(['OrderID', 'CustomerID', 'Freight', 'OrderDate'])
// Sort
.sortByDesc('Freight')
// Paginate
.skip(0)
.take(20);
const dm = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
dm.executeQuery(complexQuery).then((e) => {
console.log(`Found ${e.count} total records, showing ${e.result.length}`);
console.log(e.result);
});Query Execution Order: 1. Filter (where) 2. Select fields (select) 3. Sort (sortBy) 4. Group (group) 5. Paginate (skip/take) 6. Expand (expand)
---
Complex Filter Scenarios
Scenario 1: Date Range Filter
const startDate = new Date(1996, 6, 1); // July 1, 1996
const endDate = new Date(1996, 6, 31); // July 31, 1996
const query = new Query()
.where('OrderDate', 'greaterthanorequal', startDate)
.where('OrderDate', 'lessthanorequal', endDate)
.sortByDesc('OrderDate')
.take(50);Scenario 2: OR with Multiple Categories
import { Predicate } from '@syncfusion/ej2-data';
const statusFilter = new Predicate('Status', 'equal', 'Shipped')
.or('Status', 'equal', 'Processing')
.or('Status', 'equal', 'Pending');
const query = new Query()
.where(statusFilter)
.take(100);Scenario 3: Combine AND & OR
import { Predicate } from '@syncfusion/ej2-data';
// (CustomerID = 'VINET' OR CustomerID = 'TOMSP') AND Freight >= 50
const predicate = new Predicate('CustomerID', 'equal', 'VINET')
.or('CustomerID', 'equal', 'TOMSP')
.and('Freight', 'greaterthanorequal', 50);
const query = new Query()
.where(predicate)
.sortBy('OrderDate');Scenario 4: Search with Contains
const searchTerm = 'Synchron';
const query = new Query()
.where('CompanyName', 'contains', searchTerm)
.orWhere('ContactName', 'contains', searchTerm)
.take(20);
// This searches either CompanyName or ContactName containing the term---
Performance Tips
- Use select() on remote queries to reduce payload
- Use take() to limit results instead of fetching all
- Use expand() instead of separate queries for related data
- Filter on server with where() for large datasets
- Cache results with DataManager.enableCache