useGantt and useGanttState
React only. useGantt(options) is <Gantt> without the div: it returns the chart’s instance,
and you render the element the chart fills and the place of its slots yourself.
useGanttState(instance, selector) reads a slice of the chart’s state while a component renders,
and renders it again only when that slice changes. Together they let a toolbar live outside the
chart: the one above the chart here is a component of its own, which gets the instance, shows the
chart’s state and calls its methods. The component that owns the chart (Example) is not
subscribed to anything, so it does not render again when the chart’s state changes.
pnpm add @ganttlane/react react react-domnpm install @ganttlane/react react react-domyarn add @ganttlane/react react react-dombun add @ganttlane/react react react-domimport type { Link, Task } from '@ganttlane/core';
// 2026-03-02 is a Monday.export const tasks: Task[] = [ { id: 'brief', name: 'Brief', start: '2026-03-02', end: '2026-03-04', progress: 100 }, { id: 'design', name: 'Design', start: '2026-03-05', end: '2026-03-13', progress: 70 }, { id: 'backend', name: 'Backend', start: '2026-03-16', end: '2026-03-27', progress: 40 }, { id: 'frontend', name: 'Frontend', start: '2026-03-16', end: '2026-04-03', progress: 20 }, { id: 'docs', name: 'Docs', start: '2026-03-30', end: '2026-04-03' }, { id: 'qa', name: 'QA', start: '2026-04-06', end: '2026-04-10' }, { id: 'release', name: 'Release', type: 'milestone', start: '2026-04-13' },];
export const links: Link[] = [ { source: 'brief', target: 'design' }, { source: 'design', target: 'backend' }, { source: 'design', target: 'frontend' }, { source: 'backend', target: 'qa' }, { source: 'frontend', target: 'qa' }, { source: 'docs', target: 'qa' }, { source: 'qa', target: 'release' },];import type { GanttOptions } from '@ganttlane/dom';
export const options = { // The scale of the timeline. (`useGantt` takes it as `viewMode`, controlled, or as // `defaultViewMode`, which the chart then owns: see `react.tsx`.) viewMode: 'week', // Record every change, so that `canUndo` and `canRedo` have something to say. history: true, // Open on the plan. The default, 'today', is far from these dates. scroll: { initial: 'first-task' },} satisfies Partial<GanttOptions>;import '@ganttlane/react/styles.css';
import { type GanttInstance, useGantt, useGanttState } from '@ganttlane/react';
import { links, tasks } from './data';import { options } from './options';
const VIEW_MODES = ['day', 'week', 'month'];const day = new Intl.DateTimeFormat('en', { day: 'numeric', month: 'short' });
type Range = ReturnType<GanttInstance['getVisibleRange']>;
/** Two visible ranges are the same when both edges are the same moment. */const sameRange = (a: Range, b: Range): boolean => a.start.getTime() === b.start.getTime() && a.end.getTime() === b.end.getTime();
/** * A toolbar that lives outside the chart. It gets the instance `useGantt` returned (the same * object for the life of the component), reads the state it shows with `useGanttState`, and calls * the methods on it. It renders again only when one of these slices changes: not while the chart * scrolls or a bar is dragged, and not when the selection changes. */function Toolbar({ gantt }: { gantt: GanttInstance }) { // A selector gets the chart's snapshot: here its view mode. const mode = useGanttState(gantt, (snapshot) => snapshot.viewMode.id); // History and the edit session are read from the instance. They change together with the // snapshot, and the selector runs again whenever the snapshot does. const canUndo = useGanttState(gantt, () => gantt.canUndo); const canRedo = useGanttState(gantt, () => gantt.canRedo); const editing = useGanttState(gantt, () => gantt.edit.isOpen); const dirty = useGanttState(gantt, () => gantt.edit.isDirty);
return ( <div role="toolbar" aria-label="Chart controls"> <label> View{' '} <select value={mode} onChange={(event) => { gantt.setViewMode(event.target.value); }} > {VIEW_MODES.map((id) => ( <option key={id} value={id}> {id} </option> ))} </select> </label> <button type="button" disabled={!canUndo} onClick={() => { gantt.undo(); }} > Undo </button> <button type="button" disabled={!canRedo} onClick={() => { gantt.redo(); }} > Redo </button> <button type="button" disabled={editing} onClick={() => { gantt.edit.begin(); }} > Edit </button> <button type="button" disabled={!dirty} onClick={() => { gantt.edit.commit(); }} > Save </button> <button type="button" disabled={!editing} onClick={() => { gantt.edit.rollback(); }} > Discard </button> <span>{dirty ? 'Unsaved changes' : editing ? 'Editing' : 'Viewing'}</span> </div> );}
/** What the chart shows right now, under it: the dates in view, and the selected tasks. */function Readout({ gantt }: { gantt: GanttInstance }) { // `getVisibleRange()` makes a new object on every call: `isEqual` says when two are the same. const range = useGanttState(gantt, () => gantt.getVisibleRange(), sameRange); // Parts of the snapshot keep their identity until they change, so the default `Object.is` does. const selected = useGanttState(gantt, (snapshot) => snapshot.selection.tasks); const names = selected.map((id) => gantt.getTask(id)?.name ?? String(id));
return ( <p> In view: {day.format(range.start)} to {day.format(range.end)}. Selected:{' '} {names.length > 0 ? names.join(', ') : 'none'}. </p> );}
export default function Example() { // `viewMode` is a controlled prop in React: the chart would show the one your state holds. The // toolbar changes it through the instance, so the chart starts from `defaultViewMode` and owns // the mode; `defaultTasks` and `defaultLinks` are where it starts with the data. const { viewMode, ...rest } = options;
const gantt = useGantt({ ...rest, defaultViewMode: viewMode, defaultTasks: tasks, defaultLinks: links, // A tooltip as React. It is drawn through portals that `slots` holds (below). renderTooltip: ({ task }) => ( <span> {task.name}: {Math.round(task.progress)}% </span> ), });
// `useGantt` is `<Gantt>` without the `div`: the element the chart fills is yours, and so is // the place of the slots. Destructured, which keeps the `react-hooks/refs` rule quiet about the // ref callback. const { setContainer, slots } = gantt;
return ( <> <Toolbar gantt={gantt} /> <div ref={setContainer} style={{ height: 400 }} /> {slots} <Readout gantt={gantt} /> </> );}Files
data.ts: seven tasks and the links between them.options.ts: the view mode,historyand the first scroll.react.tsx:Example(the chart, withuseGantt),ToolbarandReadout(the state, withuseGanttState).
Options
| Option | Value | What it does |
|---|---|---|
defaultViewMode |
'week' |
The scale of the timeline; the chart owns it, so the toolbar can change it |
history |
true |
Records every change, so that canUndo and canRedo have something to say |
scroll.initial |
'first-task' |
Opens on the plan; the default, 'today', is far from these dates |
defaultTasks, defaultLinks |
the tasks and links | Where the chart starts with its data; it owns it from then on |
renderTooltip |
a function | A tooltip as React; it is what makes the slots element needed |
What to try
- View mode. The select shows the mode the chart is in, and changes it. The dates in view under the chart change with it.
- The dates in view. Scroll the chart sideways: the line under it follows. It reads
getVisibleRange(), which makes a new object on every call, so the selector is given anisEqualto say when two ranges are the same. Nothing else renders while you scroll. - Undo and redo. Drag a bar: Undo turns on. Press it, and Redo turns on. Neither is a
prop of anything:
useGanttStatereadscanUndoandcanRedoagain whenever the chart changes. - Dirty. Edit opens an edit session, and the first drag in it makes the session dirty: the label says Unsaved changes and Save turns on. Discard puts every bar back (the chart owns the data, so it does that by itself).
- Selected. The line under the chart also lists the selected tasks, from
snapshot.selection.tasks. Nothing in this chart selects a task by itself (a click on a bar does not), butselect(['qa'])on the instance would show up there.
Notes
setContainerandslots.setContaineris the ref of the element the chart fills, which needs a height.slotsis a stable element that draws the portals ofrenderTooltip,renderLinkMenuandrenderBarLabel: render it next to the container, below the providers their content should see. Destructure the two from the instance, as here: thereact-hooks/refslint rule otherwise readsgantt.setContaineras a ref read during render.- The instance is one object. It is the same for the life of the component, also under StrictMode, so it can be passed down as a prop and nothing renders because of it.
- A selector gets the snapshot.
(snapshot) => snapshot.viewMode.id,snapshot.selection.tasks,snapshot.state.session, and so on. The defaultisEqualisObject.is: pick primitives, or parts of the snapshot, which keep their identity until they change. A selector that builds a new object or array each time needs anisEqual, or the component renders on every change of the chart. - A selector may read the instance.
canUndo,canRedoandedit.isDirtyare not in the snapshot, but they change together with it, so() => gantt.canUndois a valid selector. - One component, one render. A component renders again when any of the slices it reads
changes, so
Toolbar(five slices) renders on an undo, a drag that is dropped, or a new view mode, and not on a scroll;Readoutrenders on a scroll. Split what changes often from what does not, as here. - A controlled mode is the other choice.
viewModeis a controlled option:useGantt({ viewMode })would make your state the truth, andgantt.setViewMode()would be put back unless you update that state inonViewModeChange. This example lets the chart own the mode (defaultViewMode); Controlled state keeps it yours. - Anything with a snapshot works.
useGanttStatetakes a chart fromuseGantt, itscore, a core fromcreateGanttCore, or therefhandle of<Gantt>(The ref handle).