Virtual Scrolling in React Scheduler
Virtual scrolling improves Scheduler performance when you work with a large number of resources and events. It loads resources and appointments dynamically in timeline views as users scroll, which provides a smoother experience. Enable virtual scrolling by setting the allowVirtualScrolling property to true in the required timeline view settings. In Agenda view, set the same property in the agenda view configuration to enable virtual loading of events.
import * as ReactDOM from 'react-dom';
import * as React from 'react';
import { ScheduleComponent, ViewsDirective, ViewDirective, ResourcesDirective, ResourceDirective, TimelineMonth, TimelineYear, Resize, DragAndDrop, Inject } from '@syncfusion/ej2-react-schedule';
function App() {
const generateStaticEvents = (start, resCount, overlapCount) => {
let data = [];
let id = 1;
for (let i = 0; i < resCount; i++) {
let randomCollection = [];
let random = 0;
for (let j = 0; j < overlapCount; j++) {
random = Math.floor(Math.random() * (30));
random = (random === 0) ? 1 : random;
if (randomCollection.indexOf(random) !== -1 || randomCollection.indexOf(random + 2) !== -1 ||
randomCollection.indexOf(random - 2) !== -1) {
random += (Math.max.apply(null, randomCollection) + 10);
}
for (let k = 1; k <= 2; k++) {
randomCollection.push(random + k);
}
let startDate = new Date(start.getFullYear(), start.getMonth(), random);
startDate = new Date(startDate.getTime() + (((random % 10) * 10) * (1000 * 60)));
let endDate = new Date(startDate.getTime() + ((1440 + 30) * (1000 * 60)));
data.push({
Id: id,
Subject: 'Event #' + id,
StartTime: startDate,
EndTime: endDate,
IsAllDay: (id % 10) ? false : true,
ResourceId: i + 1
});
id++;
}
}
return data;
}
const eventSettings = { dataSource: generateStaticEvents(new Date(2026, 4, 1), 300, 12) }
const group = { resources: ['Resources'] };
const generateResourceData = (startId, endId, text) => {
let data = [];
let colors = [
'#ff8787', '#9775fa', '#748ffc', '#3bc9db', '#69db7c',
'#fdd835', '#748ffc', '#9775fa', '#df5286', '#7fa900',
'#fec200', '#5978ee', '#00bdae', '#ea80fc'
];
for (let a = startId; a <= endId; a++) {
let n = Math.floor(Math.random() * colors.length);
data.push({
Id: a,
Text: text + ' ' + a,
Color: colors[n]
});
}
return data;
}
return (<ScheduleComponent cssClass='virtual-scrolling' width='100%' height='550px' selectedDate={new Date(2026, 4, 1)} eventSettings={
eventSettings} group={group}>
<ResourcesDirective>
<ResourceDirective field='ResourceId' title='Resource' name='Resources' allowMultiple={true} dataSource={generateResourceData(1, 300, 'Resource')} textField='Text' idField='Id' colorField='Color'>
</ResourceDirective>
</ResourcesDirective>
<ViewsDirective>
<ViewDirective option='TimelineMonth' allowVirtualScrolling={true} isSelected={true} />
<ViewDirective option='TimelineYear' orientation='Vertical' allowVirtualScrolling={true} />
</ViewsDirective>
<Inject services={[TimelineMonth, TimelineYear, Resize, DragAndDrop]} />
</ScheduleComponent>);
};
export default App;import * as ReactDOM from 'react-dom';
import * as React from 'react';
import {
ScheduleComponent, ViewsDirective, ViewDirective, ResourcesDirective,
ResourceDirective, TimelineMonth, TimelineYear, Resize, DragAndDrop, Inject, EventSettingsModel, GroupModel
} from '@syncfusion/ej2-react-schedule';
function App() {
const generateStaticEvents = (start: Date, resCount: number, overlapCount: number): Object[] => {
let data: Object[] = [];
let id: number = 1;
for (let i: number = 0; i < resCount; i++) {
let randomCollection: number[] = [];
let random: number = 0;
for (let j: number = 0; j < overlapCount; j++) {
random = Math.floor(Math.random() * (30));
random = (random === 0) ? 1 : random;
if (randomCollection.indexOf(random) !== -1 || randomCollection.indexOf(random + 2) !== -1 ||
randomCollection.indexOf(random - 2) !== -1) {
random += (Math.max.apply(null, randomCollection) + 10);
}
for (let k: number = 1; k <= 2; k++) {
randomCollection.push(random + k);
}
let startDate: Date = new Date(start.getFullYear(), start.getMonth(), random);
startDate = new Date(startDate.getTime() + (((random % 10) * 10) * (1000 * 60)));
let endDate: Date = new Date(startDate.getTime() + ((1440 + 30) * (1000 * 60)));
data.push({
Id: id,
Subject: 'Event #' + id,
StartTime: startDate,
EndTime: endDate,
IsAllDay: (id % 10) ? false : true,
ResourceId: i + 1
});
id++;
}
}
return data;
}
const eventSettings: EventSettingsModel = { dataSource: generateStaticEvents(new Date(2026, 4, 1), 300, 12) };
const group: GroupModel = { resources: ['Resources'] };
const generateResourceData = (startId: number, endId: number, text: string): Object[] => {
let data: { [key: string]: Object }[] = [];
let colors: string[] = [
'#ff8787', '#9775fa', '#748ffc', '#3bc9db', '#69db7c',
'#fdd835', '#748ffc', '#9775fa', '#df5286', '#7fa900',
'#fec200', '#5978ee', '#00bdae', '#ea80fc'
];
for (let a: number = startId; a <= endId; a++) {
let n: number = Math.floor(Math.random() * colors.length);
data.push({
Id: a,
Text: text + ' ' + a,
Color: colors[n]
});
}
return data;
}
return (
<ScheduleComponent cssClass='virtual-scrolling' width='100%'
height='550px' selectedDate={new Date(2026, 4, 1)}
eventSettings={eventSettings}
group={group} >
<ResourcesDirective>
<ResourceDirective field='ResourceId' title='Resource' name='Resources' allowMultiple={true}
dataSource={generateResourceData(1, 300, 'Resource')}
textField='Text' idField='Id' colorField='Color'>
</ResourceDirective>
</ResourcesDirective>
< ViewsDirective >
<ViewDirective option='TimelineMonth' allowVirtualScrolling={true} isSelected={true} />
<ViewDirective option='TimelineYear' orientation='Vertical' allowVirtualScrolling={true} />
</ViewsDirective>
< Inject services={[TimelineMonth, TimelineYear, Resize, DragAndDrop]} />
</ScheduleComponent>
);
};
export default App;Note: Virtual loading of resources and events is not supported in
MonthAgenda,Year, andTimelineYear(horizontal orientation) views.
Enabling lazy loading for appointments
Lazy loading provides an efficient way to load appointment data on demand. It helps you work with large volumes of data without performance issues.
By default, the Scheduler retrieves all appointments within the current date range from the server. When lazy loading is enabled, the Scheduler sends queries to the server to fetch appointments only for the resources currently visible after scroll actions. These queries include the resource IDs and the current date range as a comma-separated string. On the server, the resource IDs are parsed to filter and return only the appointments needed for rendering.
With this feature enabled, the Scheduler fetches data from remote services only for the appointments visible in the viewport. Additional appointment data is loaded on demand as new resources enter the viewport during scrolling.
Enable this feature by setting the enableLazyLoading property to true in the appropriate view settings.
import * as ReactDOM from 'react-dom';
import * as React from 'react';
import { ScheduleComponent, ViewsDirective, ViewDirective, ResourcesDirective, ResourceDirective, TimelineMonth, Inject } from '@syncfusion/ej2-react-schedule';
import { DataManager, WebApiAdaptor } from '@syncfusion/ej2-data';
function App() {
const dataManager = new DataManager({
url: 'https://services.syncfusion.com/react/production/api/VirtualEventData',
adaptor: new WebApiAdaptor,
crossDomain: true
});
const eventSettings = { dataSource: dataManager }
const group = { resources: ['Resources'] };
const generateResourceData = (startId, endId, text) => {
let data = [];
let colors = [
'#ff8787', '#9775fa', '#748ffc', '#3bc9db', '#69db7c',
'#fdd835', '#748ffc', '#9775fa', '#df5286', '#7fa900',
'#fec200', '#5978ee', '#00bdae', '#ea80fc'
];
for (let a = startId; a <= endId; a++) {
let n = Math.floor(Math.random() * colors.length);
data.push({
Id: a,
Text: text + ' ' + a,
Color: colors[n]
});
}
return data;
}
return (<ScheduleComponent width='100%' height='550px' selectedDate={new Date(2023, 3, 1)} eventSettings={
eventSettings} group={group} readonly={true}>
<ResourcesDirective>
<ResourceDirective field='ResourceId' title='Resource' name='Resources' dataSource={generateResourceData(1, 1000, 'Resource')} textField='Text' idField='Id' colorField='Color'>
</ResourceDirective>
</ResourcesDirective>
<ViewsDirective>
<ViewDirective option='TimelineMonth' enableLazyLoading={true} isSelected={true} />
= </ViewsDirective>
<Inject services={[TimelineMonth]} />
</ScheduleComponent>);
};
export default App;import * as ReactDOM from 'react-dom';
import * as React from 'react';
import {
ScheduleComponent, ViewsDirective, ViewDirective, ResourcesDirective,
ResourceDirective, TimelineMonth, Inject, EventSettingsModel, GroupModel
} from '@syncfusion/ej2-react-schedule';
import { DataManager, WebApiAdaptor } from '@syncfusion/ej2-data';
function App() {
const dataManager: DataManager = new DataManager({
url: 'https://services.syncfusion.com/react/production/api/VirtualEventData',
adaptor: new WebApiAdaptor,
crossDomain: true
});
const eventSettings: EventSettingsModel = { dataSource: dataManager };
const group: GroupModel = { resources: ['Resources'] };
const generateResourceData = (startId: number, endId: number, text: string): Object[] => {
let data: { [key: string]: Object }[] = [];
let colors: string[] = [
'#ff8787', '#9775fa', '#748ffc', '#3bc9db', '#69db7c',
'#fdd835', '#748ffc', '#9775fa', '#df5286', '#7fa900',
'#fec200', '#5978ee', '#00bdae', '#ea80fc'
];
for (let a: number = startId; a <= endId; a++) {
let n: number = Math.floor(Math.random() * colors.length);
data.push({
Id: a,
Text: text + ' ' + a,
Color: colors[n]
});
}
return data;
}
return (
<ScheduleComponent width='100%'
height='550px' selectedDate={new Date(2023, 3, 1)}
eventSettings={eventSettings}
group={group} readonly={true}>
<ResourcesDirective>
<ResourceDirective field='ResourceId' title='Resource' name='Resources'
dataSource={generateResourceData(1, 1000, 'Resource')}
textField='Text' idField='Id' colorField='Color'>
</ResourceDirective>
</ResourcesDirective>
< ViewsDirective >
<ViewDirective option='TimelineMonth' enableLazyLoading={true} isSelected={true} />
</ViewsDirective>
< Inject services={[TimelineMonth]} />
</ScheduleComponent>
);
};
export default App;The following server-side controller code demonstrates how to retrieve appointment data based on resource IDs received as query parameters:
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.OData.Query;
namespace LazyLoadingServices.Controllers
{
public class VirtualEventDataController : Controller
{
private readonly EventsContext dbContext;
[HttpGet]
[EnableQuery]
[Route("api/VirtualEventData")]
public IActionResult GetData([FromQuery] Params param)
{
IQueryable<EventData> query = dbContext.Events;
// Filter the appointment data based on the ResourceId query params.
if (!string.IsNullOrEmpty(param.ResourceId))
{
string[] resourceId = param.ResourceId.Split(',');
query = query.Where(data => resourceId.Contains(data.ResourceId.ToString()));
}
return Ok(query.ToList());
}
}
public class Params
{
public DateTime? StartDate { get; set; }
public DateTime? EndDate { get; set; }
public string ResourceId { get; set; }
}
}Note:
- This property is effective when a large number of resources and appointments are bound to the Scheduler.
- This property is applicable only when resource grouping is enabled in the Scheduler.