Virtual scrolling in React Schedule component

21 Sep 202324 minutes to read

To achieve better performance in the Scheduler when loading a large number of resources and events, we have added virtual scrolling support to load a large set of resources and events instantly as you scroll. You can dynamically load large number of resources and events in the Scheduler by setting true to the allowVirtualScrolling property within the view specific settings. The virtual loading of events is possible in Agenda view, by setting allowVirtualScrolling property to true within the agenda view specific settings.

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';
const 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(2018, 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(2018, 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>);
}
const root = ReactDOM.createRoot(document.getElementById('schedule'));
root.render(<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';

const 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(2018, 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(2018, 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>
  );

}
const root = ReactDOM.createRoot(document.getElementById('schedule'));
root.render(<App />);
<!DOCTYPE html>
<html lang="en">

<head>
    <title>Syncfusion React Schedule</title>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="description" content="Essential JS 2 for React Components" />
    <meta name="author" content="Syncfusion" />
    <link href="https://cdn.syncfusion.com/ej2/25.1.35/ej2-base/styles/material.css" rel="stylesheet" />
    <link href="https://cdn.syncfusion.com/ej2/25.1.35/ej2-react-buttons/styles/material.css" rel="stylesheet" />
    <link href="https://cdn.syncfusion.com/ej2/25.1.35/ej2-react-calendars/styles/material.css" rel="stylesheet" />
    <link href="https://cdn.syncfusion.com/ej2/25.1.35/ej2-react-dropdowns/styles/material.css" rel="stylesheet" />
    <link href="https://cdn.syncfusion.com/ej2/25.1.35/ej2-react-inputs/styles/material.css" rel="stylesheet" />
    <link href="https://cdn.syncfusion.com/ej2/25.1.35/ej2-react-navigations/styles/material.css" rel="stylesheet" />
    <link href="https://cdn.syncfusion.com/ej2/25.1.35/ej2-react-popups/styles/material.css" rel="stylesheet" />
    <link href="https://cdn.syncfusion.com/ej2/25.1.35/ej2-react-schedule/styles/material.css" rel="stylesheet" />
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" />
    <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/0.19.38/system.js"></script>
    <link href="index.css" rel="stylesheet" />
    <script src="systemjs.config.js"></script>
    <style>
        #loader {
            color: #008cff;
            height: 40px;
            left: 45%;
            position: absolute;
            top: 45%;
            width: 30%;
        }

        .e-past-app {
            background-color: chocolate !important;
        }

        .custom-class.e-schedule .e-vertical-view .e-all-day-appointment-wrapper .e-appointment,
        .custom-class.e-schedule .e-vertical-view .e-day-wrapper .e-appointment,
        .custom-class.e-schedule .e-month-view .e-appointment {
            background: green;
        }
    </style>
<script src="https://cdn.syncfusion.com/ej2/syncfusion-helper.js" type ="text/javascript"></script>
</head>

<body>
    <div id='schedule'>
        <div id='loader'>Loading....</div>
    </div>
</body>

</html>

For now, the virtual loading of resources and events is not supported in MonthAgenda, Year and TimelineYear (Horizontal Orientation) views.

Enabling lazy loading for appointments

The lazy loading feature provides a convenient way to efficiently load resource appointments into the Scheduler using an on-demand approach. With this feature, you can seamlessly load a large volume of appointment data into the Scheduler without experiencing any performance degradation.

By default, the Scheduler fetches all the relevant appointments from the server with in the current date range. However, enabling this feature will trigger query requests to the server for appointment retrieval whenever new resources are rendered due to scroll actions. These queries contain the resource IDs of currently displayed resources along with current date range, which can be passed as a comma-separated string. In the server controller, these resource IDs are parsed to filter the necessary appointments to render in the scheduler.

When you enable this feature, the Scheduler becomes capable of fetching events from remote services only for the current view port alone to optimize the data retrieval. The remaining appointment data is fetched form the server on-demand based on currently rendered view port resources as you scroll’s through the scheduler content.

To enable this feature, you have to set the enableLazyLoading property to true within the view specific 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';

const 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>);
}
const root = ReactDOM.createRoot(document.getElementById('schedule'));
root.render(<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';

const 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>
  );

}
const root = ReactDOM.createRoot(document.getElementById('schedule'));
root.render(<App />);
<!DOCTYPE html>
<html lang="en">

<head>
    <title>Syncfusion React Schedule</title>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="description" content="Essential JS 2 for React Components" />
    <meta name="author" content="Syncfusion" />
    <link href="https://cdn.syncfusion.com/ej2/25.1.35/ej2-base/styles/material.css" rel="stylesheet" />
    <link href="https://cdn.syncfusion.com/ej2/25.1.35/ej2-react-buttons/styles/material.css" rel="stylesheet" />
    <link href="https://cdn.syncfusion.com/ej2/25.1.35/ej2-react-calendars/styles/material.css" rel="stylesheet" />
    <link href="https://cdn.syncfusion.com/ej2/25.1.35/ej2-react-dropdowns/styles/material.css" rel="stylesheet" />
    <link href="https://cdn.syncfusion.com/ej2/25.1.35/ej2-react-inputs/styles/material.css" rel="stylesheet" />
    <link href="https://cdn.syncfusion.com/ej2/25.1.35/ej2-react-navigations/styles/material.css" rel="stylesheet" />
    <link href="https://cdn.syncfusion.com/ej2/25.1.35/ej2-react-popups/styles/material.css" rel="stylesheet" />
    <link href="https://cdn.syncfusion.com/ej2/25.1.35/ej2-react-schedule/styles/material.css" rel="stylesheet" />
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" />
    <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/0.19.38/system.js"></script>
    <link href="index.css" rel="stylesheet" />
    <script src="systemjs.config.js"></script>
    <style>
        #loader {
            color: #008cff;
            height: 40px;
            left: 45%;
            position: absolute;
            top: 45%;
            width: 30%;
        }
    </style>
<script src="https://cdn.syncfusion.com/ej2/syncfusion-helper.js" type ="text/javascript"></script>
</head>

<body>
    <div id='schedule'>
        <div id='loader'>Loading....</div>
    </div>
</body>

</html>

Here’s the server-side controller code that retrieves appointment data based on the resource IDs provided 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; }
    }
}
  • The property will be effective, when large number of resources and appointments bound to the Scheduler.
  • This property is applicable only when resource grouping is enabled in Scheduler.

See Also

You can refer to our React Scheduler feature tour page for its groundbreaking feature representations. You can also explore our React Scheduler example to knows how to present and manipulate data.