Skip to content

the-curve-consulting/react-scheduler

 
 

Repository files navigation

@the-curve-consulting/react-scheduler

Open sourced, typescript oriented, light-weight, and ultra fast React Component for creating gantt charts.


About this fork

@the-curve-consulting/react-scheduler is a maintained fork of Bitnoise/react-scheduler, published by The Curve Consulting with ongoing fixes and updates. Credit for the original library goes to the Bitnoise team.


Installation

# yarn
yarn add '@the-curve-consulting/react-scheduler'
# npm
npm install '@the-curve-consulting/react-scheduler'

This package expects react, react-dom, and styled-components from the consuming app.

Releasing to GitHub Packages

This repository is configured to publish @the-curve-consulting/react-scheduler to GitHub Packages when you publish a GitHub release.

  1. Develop changes on a feature branch and merge them into master through a pull request.

  2. Check out master locally and make sure it is up to date.

  3. Update the package version. This creates both a commit and a Git tag:

npm version patch

Use patch, minor, or major depending on the change.

  1. Push master and the new version tag:
git push origin master --follow-tags
  1. In GitHub, open the repository Releases page and create a new release from the tag that npm version created, for example v0.3.1.

  2. Publishing that release triggers .github/workflows/publish-package.yml, which builds the library and publishes it to GitHub Packages using the repository GITHUB_TOKEN.

  3. After the workflow finishes, verify the package under the repository or org Packages page in GitHub.

Using in another repository

  1. Create a personal access token (classic) with at least read:packages.

  2. In the consuming repository, add an .npmrc file with:

@the-curve-consulting:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_PACKAGES_TOKEN}
  1. Set GITHUB_PACKAGES_TOKEN in your shell, local environment, or CI secrets.

  2. Install the package:

npm install @the-curve-consulting/react-scheduler
  1. Import it in the app:
import "@the-curve-consulting/react-scheduler/dist/style.css";
import { Scheduler } from "@the-curve-consulting/react-scheduler";

GITHUB_PACKAGES_TOKEN should be a personal access token (classic) with at least read:packages. If the package is private, the token also needs access to the owning org or repository.

Example usage

  1. import required styles for scheduler
import "@the-curve-consulting/react-scheduler/dist/style.css";
  1. Import Scheduler component into your project
import { useCallback, useMemo, useRef, useState } from "react";
import {
  FetchDataParams,
  ProjectDeleteUpdate,
  ProjectUpdate,
  Scheduler,
  SchedulerData,
  SchedulerHandle,
  SchedulerProjectData,
  SchedulerProps
} from "@the-curve-consulting/react-scheduler";

type PlanningMeta = {
  scenarioId: string;
  externalId: string;
};

export default function Component() {
  const schedulerRef = useRef<SchedulerHandle<PlanningMeta>>(null);
  const [selectedUserIds, setSelectedUserIds] = useState<string[]>([]);
  const scenarioId = "scenario-1";

  const handleFetchData = useCallback(
    async ({ range, signal }: FetchDataParams): Promise<SchedulerData<PlanningMeta>> => {
      const response = await fetch(
        `/api/planning?startDate=${range.startDate.toISOString()}&endDate=${range.endDate.toISOString()}&scenarioId=${scenarioId}`,
        { signal }
      );

      if (!response.ok) {
        throw new Error(`Failed to load planning data: ${response.status}`);
      }

      return response.json();
    },
    [scenarioId]
  );

  const transformData = useCallback(
    (rows: SchedulerData<PlanningMeta>) =>
      selectedUserIds.length ? rows.filter((row) => selectedUserIds.includes(row.id)) : rows,
    [selectedUserIds]
  );

  const handleTileClick = useCallback((project: SchedulerProjectData<PlanningMeta>) => {
    console.log(project.meta?.externalId);
  }, []);

  const handleHolidayClick: NonNullable<SchedulerProps<PlanningMeta>["onHolidayClick"]> =
    useCallback((holidayRequest) => {
      console.log(holidayRequest.id, holidayRequest.state);
    }, []);

  const optimisticProject: ProjectUpdate<PlanningMeta> = useMemo(
    () => ({
      rowId: "user-1",
      projects: [
        {
          id: "project-1",
          title: "Project A",
          startDate: new Date("2026-03-01"),
          endDate: new Date("2026-03-07"),
          occupancy: 3600,
          meta: {
            scenarioId,
            externalId: "resource-block-1"
          }
        }
      ]
    }),
    [scenarioId]
  );

  const deletion: ProjectDeleteUpdate = {
    rowId: "user-1",
    projectIds: ["project-1"]
  };

  return (
    <section>
      <Scheduler<PlanningMeta>
        ref={schedulerRef}
        dataSourceKey={scenarioId}
        onFetchData={handleFetchData}
        transformData={transformData}
        onRangeChange={(range) => console.log("visible range", range)}
        onTileClick={handleTileClick}
        onHolidayClick={handleHolidayClick}
        onItemClick={(item) => console.log(item)}
        onFilterData={() => setSelectedUserIds(["user-1", "user-2"])}
        onClearFilterData={() => setSelectedUserIds([])}
        config={{
          zoom: 0,
          filterButtonState: selectedUserIds.length ? 1 : 0
        }}
      />

      <button onClick={() => schedulerRef.current?.upsertProjects([optimisticProject])}>
        Optimistically add project
      </button>
      <button onClick={() => schedulerRef.current?.deleteProjects([deletion])}>
        Remove project
      </button>
      <button onClick={() => schedulerRef.current?.invalidate()}>Refetch current range</button>
    </section>
  );
}
  1. If some problems occur, please see our troubleshooting section below.

Scheduler API

Scheduler Component Props
Property Name Type Arguments Description
data SchedulerData - scheduler rows to display in static mode
initialData SchedulerData - optional initial cache seed for async mode when using onFetchData
isLoading boolean - external loading flag; forces blocking loading state
startDate string ISO date string initial date to center scheduler on mount
dataSourceKey string - async cache identity; changing it invalidates cached prefetched data
onRangeChange function updated startDate and endDate callback fired when visible date range changes (called every scroll event)
onFetchData function range, direction, reason, signal async data source used for initial fetch, edge prefetch and hard jumps (called when insufficient cached data)
onTileClick function clicked resource data detects resource click
onHolidayClick function clicked holiday request data detects holiday tile click
onItemClick function clicked left column item data detects item click on left column
onFilterData function - callback firing when filter button was clicked
onClearFilterData function - callback firing when clear filters button was clicked (clearing button is visible only when filterButtonState is set to >0)
transformData function SchedulerData transforms cached scheduler data before rendering, useful for local filtering
config Config - object with scheduler config properties

Scheduler supports two exclusive data modes:

  • static mode: pass data
  • async mode: pass onFetchData and optionally initialData
onFetchData callback contract

Return type: Promise<SchedulerData<TMeta>>

Field Type Description
range { startDate: Date; endDate: Date} date window requested by scheduler
direction "backward" | "forward" side from which scheduler requests additional data
reason "initial" | "prefetch" | "jump" why request was created
signal AbortSignal | undefined abort signal for cancelling stale requests (recommended to handle in fetch)
Imperative API

Use ref when you want to mutate cached data without invalidating the whole scheduler.

import {
  ProjectDeleteUpdate,
  ProjectUpdate,
  SchedulerHandle
} from "@the-curve-consulting/react-scheduler";

type PlanningMeta = {
  externalId: string;
};

const schedulerRef = useRef<SchedulerHandle<PlanningMeta>>(null);

const optimisticProject: ProjectUpdate<PlanningMeta> = {
  rowId: "user-1",
  projects: [
    {
      ...project,
      meta: {
        externalId: "resource-block-1"
      }
    }
  ]
};

schedulerRef.current?.upsertProjects([optimisticProject]);

const deletion: ProjectDeleteUpdate = {
  rowId: "user-1",
  projectIds: [project.id]
};

schedulerRef.current?.deleteProjects([deletion]);
schedulerRef.current?.invalidate();

Available handle methods:

Method Arguments Description
invalidate - clears async cache for the current data source and refetches visible range
upsertProjects ProjectUpdate[] creates or updates projects inside existing rows
deleteProjects ProjectDeleteUpdate[] removes projects by id from existing rows
Custom project metadata

SchedulerProjectData is generic, so you can attach your own project payload with meta and keep it typed in onFetchData, onTileClick, transformData, and the imperative handle API.

type TimesheetMeta = {
  entryId: string;
  userId: string;
};

const project: SchedulerProjectData<TimesheetMeta> = {
  id: "entry-1",
  title: "Timesheet",
  startDate: new Date("2026-03-01"),
  endDate: new Date("2026-03-01"),
  occupancy: 3600,
  meta: {
    entryId: "ts-123",
    userId: "user-1"
  }
};
Scheduler Config Object

Property Name Type Default Description
zoom 0 or 1 or 2 0 0 - display grid divided into weeks 1 - display grid divided into days 2 - display grid divided into hours
filterButtonState number 0 < 0 - hides filter button, 0 - state for when filters were not set, > 0 - state for when some filters were set (allows to also handle onClearFilterData event)
maxRecordsPerPage number 50 number of rows (projects) from SchedulerData visible per page
lang en, lt or pl en scheduler's language
showTooltip boolean true show tooltip when hovering over tiles
translations LocaleType[] undefined option to add specific langs translations
showThemeToggle boolean false show toggle button to switch between light/dark mode
defaultTheme light or dark light scheduler's default theme
theme Theme undefined custom light/dark theme color overrides
maxHoursPerWeek number 40 fallback maximum week capacity used to derive default Monday-Friday working durations and default holiday half-day duration
defaultStartHour number 9 start hour used when placing entries and partial-day holidays in hourly view
dataLoading DataLoadingConfig built-in defaults controls prefetching and cache window used by onFetchData flow
DataLoadingConfig
Property Name Type Default Description
initialLoadDays number 90 initial range size (days) requested around first visible date
prefetchDays number 60 number of days requested for each prefetch
prefetchTriggerDays number 45 day-based threshold for triggering edge prefetch
prefetchTriggerRatio number 0.7 ratio-based threshold for triggering edge prefetch
maxCachedDays number 120 soft cache half-window retained around current view
requestDebounceMs number 80 debounce for rapid prefetch triggers
jumpWindowMultiplier number 3 multiplier for hard-jump fetch window vs visible range
minJumpWindowDays number 45 minimum hard-jump fetch window

Translation object example

import enDayjsTranslations from "dayjs/locale/en";

const langs: LocaleType[] = [
  {
    id: "en",
    lang: {
      feelingEmpty: "I feel so empty...",
      free: "Free",
      loadNext: "Next",
      loadPrevious: "Previous",
      over: "over",
      taken: "Taken",
      topbar: {
        filters: "Filters",
        next: "next",
        prev: "prev",
        today: "Today",
        view: "View"
      },
      search: "search",
      week: "week"
    },
    translateCode: "en-EN",
    dayjsTranslations: enDayjsTranslations
  }
];

<Scheduler
  // ... //
  config={{
    lang: "en",
    translations: langs
  }}
/>;

Scheduler LocaleType Object

Property Name Type Description
id string key is needed for selecting lang
lang Translation object with translations
translateCode string code that is saved in localStorage
dayjsTranslations string ILocale undefined object with translation from dayjs

Scheduler Translation Object

Property Name Type
feelingEmpty string
free string
loadNext string
loadPrevious string
over string
taken string
search string
week string
topbar Topbar
Scheduler Topbar Object
Property Name Type
filters string
next string
prev string
today string
view string
Scheduler Data

array of chart rows with shape of

Property Name Type Description
id string unique row id
label SchedulerRowLabel row's label, e.g person's name, surname, icon
data Array<ResourceItem> array of resources
holidayRequests HolidayRequest[] row-level holiday requests used for holiday tiles and holiday-adjusted occupancy
workingDurations WorkingDuration[] (optional) row-level working hours used for occupancy, throughput, tooltips, and non-working tile segments
HolidayRequest

Rows must provide holidayRequests. Use an empty array when the row has no holidays.

Holiday requests render as background holiday tiles and are passed to onHolidayClick when clicked. They also reduce available working time used by tooltips, throughput calculations, and non-working tile segmentation. Full-day and multi-day holidays remove the affected working time. Partial-day holidays remove half of the default workday derived from config.maxHoursPerWeek, not half of the row's custom working hours. In hourly zoom, the half-day is placed from config.defaultStartHour; in daily and weekly zoom, partial holidays render as half of the visual day cell.

Scheduler does not filter holiday requests by state. Pass only the requests that should be rendered and included in capacity calculations.

For partial holidays, Morning removes the first half of the day. Other defined morning_or_afternoon values are treated as the second half of the day.

Property Name Type Description
id string unique holiday request id
leave_from Date first date covered by the holiday request
leave_to Date last date covered by the holiday request
leave_type "Sick Leave" | "Holiday / Vacation" | "Compassionate Leave" | "Unpaid Leave" | "Paternity Leave" | "Unknown" holiday category returned by the data source
state "pending" | "approved" | "rejected" | "cancelled" request state returned by the data source
morning_or_afternoon "Morning" | "Afternoon" | "Half Day" (optional) partial-day marker; omitted values are treated as full-day holiday blocks
WorkingDuration

Rows can define workingDurations when a person has custom working hours. If omitted, Scheduler defaults to Monday-Friday working days derived from config.maxHoursPerWeek (40 hours by default).

When multiple working durations are provided, Scheduler uses the latest effectiveFrom date that is not after the rendered date. This allows working hours to change over time.

Property Name Type Description
effectiveFrom Date first date from which this working duration applies
flexibleHours boolean flag preserved on the row model
workingDays WorkingDay[] weekdays and hours worked on each day
WorkingDay
Property Name Type Description
day "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday" | "Saturday" | "Sunday" day of week
hours number working hours for that day

Example row with custom working hours and mixed project duration models:

const data: SchedulerData<PlanningMeta> = [
  {
    id: "user-1",
    label: {
      icon: "JD",
      title: "Jane Doe",
      subtitle: "Consultant"
    },
    holidayRequests: [
      {
        id: "holiday-1",
        leave_from: new Date("2026-03-04"),
        leave_to: new Date("2026-03-04"),
        leave_type: "Holiday / Vacation",
        state: "approved",
        morning_or_afternoon: "Morning"
      }
    ],
    workingDurations: [
      {
        effectiveFrom: new Date("2026-01-01"),
        flexibleHours: false,
        workingDays: [
          { day: "Monday", hours: 7.5 },
          { day: "Tuesday", hours: 7.5 },
          { day: "Wednesday", hours: 7.5 },
          { day: "Thursday", hours: 7.5 },
          { day: "Friday", hours: 6 }
        ]
      }
    ],
    data: [
      {
        id: "fixed-time-project",
        title: "Fixed time project",
        startDate: new Date("2026-03-02"),
        endDate: new Date("2026-03-06"),
        occupancy: 3600
      },
      {
        id: "throughput-project",
        title: "Throughput project",
        startDate: new Date("2026-03-02"),
        endDate: new Date("2026-03-06"),
        throughput: 0.8
      }
    ]
  }
];
Left Colum Item Data

data that is accessible as argument of onItemClick callback

Property Name Type Description
id string unique row id
label SchedulerRowLabel row's label, e.g person's name, surname, icon
Resource Item

item that will be visible on the grid as tile and that will be accessible as argument of onTileClick event

Property Name Type Description
id string unique resource id
title string resource title that will be displayed on resource tile
subtitle string (optional) resource subtitle that will be displayed on resource tile
description string (optional) resource description that will be displayed on resource tile
startDate Date date for calculating start position for resource
endDate Date date for calculating end position for resource
occupancy number fixed number of seconds the resource takes per working day
throughput number fraction of the person's holiday-adjusted working day that the resource takes, e.g. 0.8 means 80% of available working hours
bgColor string (optional) tile color
meta TMeta (optional) custom project payload preserved by scheduler and exposed in typed callbacks

Provide either occupancy or throughput for a resource item.

Throughput is calculated per available working day. Multiple throughput resources whose total is greater than 1 can create overtime; the scheduler does not cap the requested work at 100% before calculating tooltip overtime.

Troubleshooting

  • For using Scheduler with RemixJS make sure to add @the-curve-consulting/react-scheduler to serverDependenciesToBundle in remix.config.js like so:
// remix.config.js
/** @type  {import('@remix-run/dev').AppConfig} */
module.exports = {
	// ...
	serverDependenciesToBundle: [..., "@the-curve-consulting/react-scheduler"],
};
  • When using with NextJS (app router) Scheduler needs to be wrapped with component with use client
"use client"
import { Scheduler, SchedulerProps } from "@the-curve-consulting/react-scheduler";

default export function SchedulerClient(props: SchedulerProps) {
	return <Scheduler {...props} />;
}
  • When using with NextJS (pages router) it needs to be imported using dynamic:
import dynamic from "next/dynamic";
const Scheduler = dynamic(
  () => import("@the-curve-consulting/react-scheduler").then((mod) => mod.Scheduler),
  {
    ssr: false
  }
);
  • How to customize Scheduler dimensions

Scheduler is position absolutely to take all available space. If you want to have fixed dimensions wrap Scheduler inside a div with position set to relative.

Example using styled components:

export const StyledSchedulerFrame = styled.div`
  position: relative;
  height: 40vh;
  width: 40vw;
`;

<StyledSchedulerFrame>
    <Scheduler  {...}/>
</StyledSchedulerFrame>

Known Issues

  1. No responsiveness
  2. Slower performance on Firefox when working with big set of data due to Firefox being slower working with canvas

How to contribute

  • Reporting Issues: If you come across any bugs, glitches, or have any suggestions for improvements, please open an issue on our GitHub repository. Provide as much detail as possible, including steps to reproduce the issue.
  • Suggesting Enhancements: If you have ideas for new features or enhancements, we would love to hear them! You can open an issue on our GitHub repository and clearly describe your suggestion.
  • Submitting Pull Requests: If you have developed a fix or a new feature that you would like to contribute, you can submit a pull request. Here's a quick overview of the process:
    • Clone the repository and create your own branch: git checkout -b feat/your-branch-name.
    • Implement your changes, following the code style and guidelines from development.md.
    • Test your changes to ensure they work as expected.
    • Commit your changes and push to your forked repository.
    • Open a pull request against our main repository's master branch.
      • add at least 1 reviewer
      • link correct issue

License

MIT Licensed. Copyright (c) Bitnoise 2023, The Curve Consulting Services Limited 2026.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages

  • TypeScript 99.8%
  • Other 0.2%