The ref handle
React only. A ref on <Gantt> is the chart’s handle: scrollToTask, setViewMode, select,
updateTask, undo, edit.begin and the other methods of the vanilla chart (without destroy,
which React does), for code that does not run while rendering, such as a click handler. The buttons
above the chart call them, and the first line under the buttons says what each call returned.
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. Nine tasks over seven weeks: more than the chart shows at once, on both// axes, so that the scroll buttons have somewhere to go.export const tasks: Task[] = [ { id: 'research', name: 'Research', start: '2026-03-02', end: '2026-03-06', progress: 100 }, { id: 'design', name: 'Design', start: '2026-03-09', end: '2026-03-13', progress: 60 }, { id: 'build', name: 'Build', start: '2026-03-16', end: '2026-03-27', progress: 30 }, { id: 'docs', name: 'Docs', start: '2026-03-23', duration: 5 }, { id: 'review', name: 'Review', start: '2026-03-30', end: '2026-04-03' }, { id: 'qa', name: 'QA', start: '2026-04-06', end: '2026-04-10' }, { id: 'marketing', name: 'Marketing', start: '2026-04-06', end: '2026-04-17' }, { id: 'support', name: 'Support', start: '2026-04-13', end: '2026-04-17' }, { id: 'launch', name: 'Launch', type: 'milestone', start: '2026-04-20' },];
export const links: Link[] = [ { source: 'research', target: 'design' }, { source: 'design', target: 'build' }, { source: 'build', target: 'review' }, { source: 'docs', target: 'review' }, { source: 'review', target: 'qa' }, { source: 'qa', target: 'support' }, { source: 'support', target: 'launch' }, { source: 'marketing', target: 'launch' },];import type { GanttOptions } from '@ganttlane/dom';
export const options = { // The scale of the timeline. (<Gantt> takes it as `viewMode`, controlled, or as // `defaultViewMode`, which the chart then owns: see `react.tsx`.) viewMode: 'day', // Record every change, so that `undo()` and `redo()` have something to step through. 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 { Gantt, type GanttHandle } from '@ganttlane/react';import { useEffect, useRef, useState } from 'react';
import { links, tasks } from './data';import { options } from './options';
/** A button: its label, the call it makes (shown in the status line), and the call itself. */type Action = readonly [label: string, call: string, run: (handle: GanttHandle) => unknown];type UpdateResult = Awaited<ReturnType<GanttHandle['updateTask']>>;
/** `1 task changed`, `5 tasks changed`: a change set lists every task it changed. */function changed(changes: readonly unknown[]): string { return `${changes.length} ${changes.length === 1 ? 'task' : 'tasks'} changed`;}
/** What `updateTask` did, in a few words. */function summarize(result: UpdateResult): string { return result.status === 'committed' ? `committed, ${changed(result.changeSet.changes)}` : result.status;}
// Every method is on the handle: `handle.setViewMode(…)`, `handle.edit.begin()`, … What they// return is shown after the arrow. These tables only describe the buttons.const VIEW: Action[] = [ ...['day', 'week', 'month'].map((id): Action => [ id, `setViewMode('${id}')`, (handle) => { handle.setViewMode(id); }, ]), [ 'Go to Launch', "scrollToTask('launch')", (handle) => { handle.scrollToTask('launch', { align: 'center' }); }, ], [ 'Select Design', "select(['design'])", (handle) => { handle.select(['design']); }, ], [ 'Clear', 'clearSelection()', (handle) => { handle.clearSelection(); }, ],];
const CHANGES: Action[] = [ [ 'Design +10%', "updateTask('design', { progress })", async (handle) => { const progress = Math.min(100, (handle.getTask('design')?.progress ?? 0) + 10); return summarize(await handle.updateTask('design', { progress })); }, ], [ 'Build +1 week', "updateTask('build', { start })", async (handle) => { const start = handle.getTask('build')?.start; // a `Date`: never mutate it if (!start) return 'no such task'; // Alone, a new `start` moves the task and keeps its length; its successors follow. const next = new Date(start.getFullYear(), start.getMonth(), start.getDate() + 7); return summarize(await handle.updateTask('build', { start: next })); }, ], ['Undo', 'undo()', (handle) => handle.undo().status], ['Redo', 'redo()', (handle) => handle.redo().status], ['Edit', 'edit.begin()', (handle) => handle.edit.begin().status], [ 'Save', 'edit.commit()', (handle) => { const result = handle.edit.commit(); return result.status === 'committed' ? `committed, ${changed(result.changes.changes)}` : result.status; }, ], ['Cancel', 'edit.rollback()', (handle) => handle.edit.rollback().status],];
function Buttons({ label, actions, onRun,}: { label: string; actions: readonly Action[]; onRun: (call: string, run: Action[2]) => void;}) { return ( <div role="toolbar" aria-label={label}> {actions.map(([name, call, run]) => ( <button key={name} type="button" onClick={() => { onRun(call, run); }} > {name} </button> ))} </div> );}
export default function Example() { // The handle: the methods of the chart, for code that does not run while rendering (a click, an // effect). It is `null` before the chart is committed and after the component unmounts. const chart = useRef<GanttHandle>(null); const [returned, setReturned] = useState( 'Press a button: this line says what the method returned.', ); const [heard, setHeard] = useState('Press Select Design, or click an arrow between two bars.');
// `on` adds a listener that is not tied to a render, and returns the function that removes it. useEffect( () => chart.current?.on('selectionChange', (tasks, link) => { setHeard(`selectionChange: tasks [${tasks.join(', ')}], link ${link ?? 'none'}`); }), [], );
/** Calls a method on the handle, then says what it returned (a string, or a promise of one). */ const onRun = (call: string, run: Action[2]): void => { const handle = chart.current; if (!handle) return; void Promise.resolve(run(handle)).then((value) => { setReturned(typeof value === 'string' ? `${call} → ${value}` : call); }); };
// `viewMode` is a controlled prop in React: the chart would show the one your state holds, and // put back what `setViewMode` on the handle sets. So the chart starts from `defaultViewMode` // and owns the mode; `defaultTasks` and `defaultLinks` are where it starts with the data. const { viewMode, ...rest } = options;
return ( <> <Buttons label="View, scrolling and selection" actions={VIEW} onRun={onRun} /> <Buttons label="Changes, history and edit session" actions={CHANGES} onRun={onRun} /> {/* What the last call returned, and what the listener heard: next to the buttons. */} <p>{returned}</p> <p>{heard}</p> <Gantt ref={chart} // The chart fills its container and scrolls inside it, so the container needs a height. style={{ height: 340, marginTop: '0.5rem' }} defaultTasks={tasks} defaultLinks={links} defaultViewMode={viewMode} {...rest} /> </> );}Files
data.ts: nine tasks over seven weeks and the links between them.options.ts: the view mode,historyand the first scroll.react.tsx: theref, a table of buttons (each is one call on the handle), a listener added withon.
Options
| Option | Value | What it does |
|---|---|---|
ref |
useRef<GanttHandle>(null) |
The handle: the methods of the chart |
defaultViewMode |
'day' |
The scale of the timeline; the chart owns it, so setViewMode sticks |
history |
true |
Records every change, so that undo() and redo() have an entry to step |
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, and updateTask changes it |
What to try
- View. Week and Month change the scale and keep the date at the centre in place. Go to Launch scrolls on both axes (the bar is below and to the right of the first screen).
- Select. Select Design selects the task in code, and its bar is drawn selected. A click on an
arrow selects a link instead (a click on a bar does not select a task). The second line under the
buttons is a listener added with
on('selectionChange', …): it hears both, as(tasks, link). - Change. Design +10% is
updateTaskwith aprogress. Build +1 week isupdateTaskwith astart: Build keeps its length and the tasks that follow it move along the links, so the status line counts more than one changed task. Both are recorded: Undo and Redo step through them, and sayemptywhen there is nothing to step to. - Edit session. Edit opens a session (
edit.begin()). Drag some bars, then Cancel puts every bar back, or Save keeps them and counts the net changes.
Notes
- The handle is for commands, not for rendering. It is the same object for the life of the
component and calling a method does not render anything. To show a part of the chart’s state in
your UI (whether
undo()can step, which tasks are selected, the visible dates) read it withuseGanttState: see Hooks. ref.currentisnullbefore the chart is committed and after the component unmounts (and while a hidden<Activity>has the chart), so writechart.current?.…. It is set before the layout effects of the parent run, so an effect may use it at once, as theoncall here does.- Listeners.
on(name, handler)takes the events of theon<Event>props (selectionChangeforonSelectionChange) and returns the function that removes the listener, which is what an effect returns. Use the props for what belongs to a render, andonfor a log or an analytics call. - Controlled props win.
viewMode,tasks,links,expandedKeysandselectedKeyshave a prop that makes the group yours. Then a method of the handle (setViewMode,select,collapse, …) calls your handler and the chart goes back to what your state says, unless the handler changes your state. Here the chart owns them (defaultViewMode,defaultTasks), so the methods stick. The other way is shown in Controlled state. - Results are values.
updateTaskresolves tocommitted(with thechangeSet),unchanged,rejected,busyorcancelled;undo()andredo()toundone/redone,empty,stale,busyorcancelled;edit.begin(),commit()androllback()to a status too (begun,already-open,committed,rolled-back,no-session, …). Nothing throws for a refused change. TDataflows through the handle:useRef<GanttHandle<Meta>>(null)for tasks with a typeddata, as in A release plan.