Skip to main content
SVAR Gantt is built for high-volume projects. Virtual scrolling is enabled by default — only the rows and chart cells currently in the viewport are rendered regardless of total task count. The demo below measures rendering 10,000 tasks spread across 3 years.

Virtual scrolling

No configuration is needed. The chart and grid virtualise both rows (vertical) and time cells (horizontal) automatically. You will not notice a difference in the component API whether you have 50 tasks or 50,000.
// Renders 10,000 tasks with default virtual scrolling.
import { Gantt } from '@svar-ui/react-gantt';

<Gantt tasks={generatedTasks} links={generatedLinks} scales={scales} />
The 10,000-task performance demo renders and measures the initial paint time on the client. See the live demo for an interactive benchmark.

Lazy loading with the lazy flag

For very deep task hierarchies you can defer loading children until a summary task is expanded. Set lazy: true on any task that has children not yet included in the initial dataset.
const tasks = [
  {
    id: 1,
    text: 'Phase 1',
    type: 'summary',
    open: false,
    lazy: true,   // Children are not yet in the tasks array.
    parent: 0,
    start: new Date(2026, 0, 1),
    duration: 90,
  },
  // Phase 1's children are loaded on demand.
];
When the user expands a lazy task, the Gantt emits a request-data event with the expanded task’s id. Respond by fetching the children and calling api.exec('provide-data', ...) to insert them:
const init = useCallback((api) => {
  setApi(api);

  api.on('request-data', (ev) => {
    fetch(`/api/tasks/${ev.id}`)
      .then((res) => res.json())
      .then(({ tasks, links }) => {
        // Dates must be parsed to Date objects before providing.
        tasks.forEach((t) => {
          t.start = new Date(t.start);
          if (t.end) t.end = new Date(t.end);
        });

        api.exec('provide-data', {
          id: ev.id,
          data: { tasks, links },
        });
      });
  });
}, []);

<Gantt init={init} tasks={initialTasks} links={initialLinks} scales={scales} />
Always parse date strings to Date objects before passing them to provide-data. The Gantt internally calls parseTaskDates() on the initial tasks prop, but data provided via provide-data arrives after that step.

Server-side data loading with RestDataProvider

@svar-ui/gantt-data-provider ships a RestDataProvider that handles the full CRUD + lazy-load cycle over a REST API. Connect it via api.setNext() so all Gantt actions (add, update, delete, lazy expand) are automatically synced to the server.
1

Install the data provider

RestDataProvider is bundled as a dependency of @svar-ui/react-gantt. Import it directly:
import { RestDataProvider } from '@svar-ui/gantt-data-provider';
2

Create a provider instance

const restProvider = useMemo(
  () => new RestDataProvider('https://api.example.com'),
  [],
);
3

Load initial data

useEffect(() => {
  restProvider.getData().then(({ tasks, links }) => {
    setTasks(tasks);
    setLinks(links);
  });
}, [restProvider]);
4

Connect the provider to the API

const init = useCallback(
  (api) => {
    setApi(api);

    // Route all Gantt actions through the provider.
    api.setNext(restProvider);

    // Handle lazy-expand requests.
    api.on('request-data', (ev) => {
      restProvider.getData(ev.id).then(({ tasks, links }) => {
        api.exec('provide-data', {
          id: ev.id,
          data: { tasks, links },
        });
      });
    });
  },
  [restProvider],
);

return (
  <ContextMenu api={api}>
    <Gantt init={init} tasks={tasks} links={links} scales={scales} />
  </ContextMenu>
);

Batched REST updates

To reduce the number of HTTP requests when many actions fire in quick succession, pass a batchURL option to RestDataProvider. All queued mutations are sent in a single batch request.
const restProvider = useMemo(
  () =>
    new RestDataProvider('https://api.example.com', {
      batchURL: 'batch', // POST to https://api.example.com/batch
    }),
  [],
);
The batch request body is an array of operation objects. Your server endpoint should process them in order and return an array of results.

Manual server sync (without RestDataProvider)

If you need full control over how changes are sent to the server, use api.on() to handle each action type:
const init = useCallback((api) => {
  setApi(api);

  api.on('add-task', ({ id, task }) =>
    fetch('/api/tasks', {
      method: 'POST',
      body: JSON.stringify({ id, ...task }),
    }),
  );

  api.on('update-task', ({ id, task }) =>
    fetch(`/api/tasks/${id}`, {
      method: 'PUT',
      body: JSON.stringify(task),
    }),
  );

  api.on('delete-task', ({ id }) =>
    fetch(`/api/tasks/${id}`, { method: 'DELETE' }),
  );

  api.on('add-link', ({ link }) =>
    fetch('/api/links', {
      method: 'POST',
      body: JSON.stringify(link),
    }),
  );

  api.on('delete-link', ({ id }) =>
    fetch(`/api/links/${id}`, { method: 'DELETE' }),
  );

  // Lazy-load children on expand.
  api.on('request-data', (ev) => {
    Promise.all([
      fetch(`/api/tasks/${ev.id}`).then((r) => r.json()),
      fetch(`/api/links/${ev.id}`).then((r) => r.json()),
    ]).then(([tasks, links]) => {
      tasks.forEach((t) => { t.start = new Date(t.start); });
      api.exec('provide-data', { id: ev.id, data: { tasks, links } });
    });
  });
}, []);

Best practices

Load only the top-level tasks and their immediate children on first render. Mark deeper summary tasks with lazy: true and load their children on demand via request-data.
The Gantt expects Date objects, not strings. Parse start and end to Date in the same place you receive server data — before passing to tasks prop or provide-data.
RestDataProvider with batchURL reduces HTTP round-trips and handles the lazy-load protocol automatically. Prefer it over manually wiring api.on() handlers unless you need custom request shapes.
The undo={true} prop on <Gantt> keeps a full history buffer in memory. Disable it on read-only charts or dashboards where undo is not required.

Build docs developers (and LLMs) love