@the-curve-consulting/react-scheduler
v0.3.10
Published
<div align="center"> <img src="logo.svg" style="height: 60px" alt="@the-curve-consulting/react-scheduler"> <hr /> <p align="center"> Open sourced, typescript oriented, light-weight, and ultra fast React Component for creating gantt charts. </p
Readme
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.
Develop changes on a feature branch and merge them into
masterthrough a pull request.Check out
masterlocally and make sure it is up to date.Update the package version. This creates both a commit and a Git tag:
npm version patchUse patch, minor, or major depending on the change.
- Push
masterand the new version tag:
git push origin master --follow-tagsIn GitHub, open the repository Releases page and create a new release from the tag that
npm versioncreated, for examplev0.3.1.Publishing that release triggers
.github/workflows/publish-package.yml, which builds the library and publishes it to GitHub Packages using the repositoryGITHUB_TOKEN.After the workflow finishes, verify the package under the repository or org Packages page in GitHub.
Using in another repository
Create a personal access token (classic) with at least
read:packages.In the consuming repository, add an
.npmrcfile with:
@the-curve-consulting:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_PACKAGES_TOKEN}Set
GITHUB_PACKAGES_TOKENin your shell, local environment, or CI secrets.Install the package:
npm install @the-curve-consulting/react-scheduler- 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
- import required styles for scheduler
import "@the-curve-consulting/react-scheduler/dist/style.css";- Import Scheduler component into your project
import { useCallback, useMemo, useRef, useState } from "react";
import {
FetchDataParams,
ProjectDeleteUpdate,
ProjectUpdate,
Scheduler,
SchedulerData,
SchedulerHandle,
SchedulerProjectData
} 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 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}
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>
);
}- 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 |
| 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
onFetchDataand optionallyinitialData
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 |
| defaultStartHour | number | 9 | start hour used when placing entries 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 |
| workingDurations | WorkingDuration[] (optional) | row-level working hours used for occupancy, throughput, tooltips, and non-working tile segments |
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"
},
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 working day that the resource takes, e.g. 0.8 means 80% of configured 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.
Troubleshooting
- For using Scheduler with RemixJS make sure to add
@the-curve-consulting/react-schedulertoserverDependenciesToBundleinremix.config.jslike 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
- No responsiveness
- 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
masterbranch.- add at least 1 reviewer
- link correct issue
- Clone the repository and create your own branch:
License
MIT Licensed. Copyright (c) Bitnoise 2023, The Curve Consulting Services Limited 2026.
