React Gantt 概览
React Gantt 在 Commercial、Enterprise 和 Ultimate 许可 下可用。
如果你使用的是 Gantt 的 Individual 或 GPL 版本,请参考 React 的 入门指南 文章。
概览
DHTMLX Gantt 是一个纯 JavaScript 组件,可以在任何浏览器环境中工作。Gantt 的 Commercial 及更高版本包括一个 React Gantt 组件,它对 DHTMLX Gantt 进行了封装,允许你在 React 中原生地使用它。
这个封装器让你在 React 应用中使用熟悉的 props/state 模型来创建一个功能完备的 Gantt 图。在内部,它管理一个标准的 DHTMLX Gantt 实例,将你的 React props(如任务、配置等)转换成相应的 Gantt 初始化和数据结构。
关键特性
- 声明式数据处理:把任务、链接、资源等作为 props 传递的数组处理。
- 配置性强:将 React props 映射到底层的 gantt.config、gantt.templates、gantt.plugins 等。
- 访问完整的 Gantt API:通过 ref 调用诸如 getTask、updateTask、或 addTaskLayer 等方法。
- 易于定制:使用 React 组件实现模板、轻量表单或内联编辑器等。
如果你是 DHTMLX Gantt 的新用户,请参阅 DHTMLX Gantt 文档 以了解诸如 Work Time Calculation、Auto Scheduling、Resource Management 等特性概览。
如果你使用 AI 代码助手,DHTMLX React Gantt agent skill 可以帮助它遵循正确的集成模式并避免常见错误。要获取实时 API 参考,请 连接 DHTMLX MCP 服务器。
安装与 NPM 访问
关于 Evaluation 和 Professional 构建的最新安装说明(包括 npm registry 配置和离线示例),请参阅 安装指南。
安装包后,你可以在 React 代码中按如下方式导入封装器:
// Evaluation 构建(公共 npm)
import ReactGantt from '@dhtmlx/trial-react-gantt';
import '@dhtmlx/trial-react-gantt/dist/react-gantt.css';
// Professional 构建(私有 npm)
import ReactGantt from '@dhx/react-gantt';
import '@dhx/react-gantt/dist/react-gantt.css';
版本要求
- React v18.0.0 或更高
基本用法
下方是一段最小的代码片段,展示如何导入并渲染 Gantt 图:
import { useState } from 'react';
import ReactGantt from '@dhx/react-gantt';
import '@dhx/react-gantt/dist/react-gantt.css';
import { demoData } from './DemoData'
export default function BasicGantt() {
const [theme, setTheme] = useState("terrace");
const [tasks, setTasks] = useState(demoData.tasks);
const [links, setLinks] = useState(demoData.links);
return (
<div style={ { height: '500px' }}>
<ReactGantt
tasks={tasks}
links={links}
theme={theme}
/>
</div>
);
}
请注意,上面这段代码片段展示的是商业版 Gantt 的用法。若要使用试用代码源,请按如下方式包含包:
import ReactGantt from '@dhtmlx/trial-react-gantt';
import '@dhtmlx/trial-react-gantt/dist/react-gantt.css';
其中 demoData 的格式如下 format:
export const demoData = {
tasks: [
{ id: 1, text: "Product Launch", type: "project", open: true, parent: 0},
{ id: 2, text: "Planning Phase", type: "project", open: true, parent: 1},
{ id: 3, text: "Requirement Gathering", type: "task", progress: 0.2,
start_date: "01-06-2025", duration: 3, parent: 2},
{ id: 4, text: "Technical Feasibility", type: "task", progress: 0.4,
start_date: "04-06-2025", duration: 2, parent: 2},
{ id: 5, text: "Implementation Phase", type: "project", progress: 0.1,
open: true, start_date: "08-06-2025", duration: 10, parent: 1},
{ id: 6, text: "Prototype Development", type: "task", progress: 0.0,
start_date: "08-06-2025", duration: 4, parent: 5},
{ id: 7, text: "Feature Testing", type: "task", progress: 0.0,
start_date: "12-06-2025", duration: 4, parent: 5},
{ id: 8, text: "Go-Live Milestone", type: "milestone", progress: 0,
start_date: "18-06-2025", duration: 0, parent: 1}
],
links: [
{ id: 1, source: 3, target: 4, type: "0" },
{ id: 2, source: 4, target: 5, type: "0" },
{ id: 3, source: 6, target: 7, type: "0" },
{ id: 4, source: 7, target: 8, type: "0" }
]
};
export {demoData};
绑定数据
React Gantt 封装器提供了加载和保存数据的灵活方法。从概念上讲,有两种主要的更改绑定模型:
- React(或一个状态管理器)作为真实数据源
- Gantt 作为真实数据源
任一方法都是有效的,但你应该选择一种并始终如一地遵循,以避免出现意外行为。
本节提供两种绑定模型的高级概览。若需要更详细的指南(包含完整示例),请参阅 Basics。
以 React(或状态管理器)作为真实数据源
在这种模式下,ReactGantt 通过 props 接收所有任务/链接数据(来自 useState、Redux、Zustand 等)。每当用户在图表中修改任务或链接时,Gantt 会调用 data.save 回调。在该回调中,你更新应用的状态。当状态变化时,React 会重新渲染 ReactGantt,并让 Gantt 实例与最新数据同步。
import { useMemo, useState } from 'react';
import ReactGantt, { type Task, type Link } from '@dhtmlx/trial-react-gantt';
import '@dhtmlx/trial-react-gantt/dist/react-gantt.css';
export function MyGanttApp({ initialTasks, initialLinks }: {
initialTasks: Task[];
initialLinks: Link[];
}) {
const [tasks, setTasks] = useState<Task[]>(initialTasks);
const [links, setLinks] = useState<Link[]>(initialLinks);
const data = useMemo(
() => ({
save: (entity: string, action: string, item: any, id: string | number) => {
if (entity === 'task') {
setTasks((prev) => {
if (action === 'create') return [...prev, item as Task];
if (action === 'update') return prev.map((task) =>
task.id === id ? (item as Task) : task
);
if (action === 'delete') return prev.filter((task) => task.id !== id);
return prev;
});
}
if (entity === 'link') {
setLinks((prev) => {
if (action === 'create') return [...prev, item as Link];
if (action === 'update') return prev.map((link) =>
link.id === id ? (item as Link) : link
);
if (action === 'delete') return prev.filter((link) => link.id !== id);
return prev;
});
}
},
})),
[]
);
return (
<ReactGantt
tasks={tasks}
links={links}
data={data}
/>
);
}
这种方式使你的 React(或全局)状态成为唯一的数据来源。它与 Redux Toolkit、Zustand、MobX、Jotai、XState 或 Valtio 等状态管理工具天然协作——你只需用你的商店钩子/选择器替换 useState,并将更新逻辑移入到商店中。
更多示例(包括与特定管理器的集成)请参阅 React 状态作为真相来源。
将 Gantt 作为真实数据源
在这种方法中,Gantt 本身 持有数据的权威副本。你仍然通过 props 或 URL 初始化或加载任务和链接,但一旦图表运行,Gantt 会在内部处理变更并将更新转发到你的后端或自定义处理程序,而不是在每次编辑时通过 React 状态进行。
import ReactGantt from '@dhtmlx/trial-react-gantt';
import '@dhtmlx/trial-react-gantt/dist/react-gantt.css';
export function GanttTransportExample() {
return (
<ReactGantt
data={{
load: '/api/gantt/data', // Gantt 通过该端点加载任务/链接
save: '/api/gantt/data', // Gantt 将变更发送回这里
}}
/>
);
}
在这种模式下:
- 本地 Gantt 实例保留当前数据的权威拷贝
- React 不会在每次任务/链接变动时重新渲染
- 像自动排程这样的批量操作成本更低,因为它们不会触发重复的 React 更新
如果你仍然在 React 状态中保留某些任务/链接的表示,请小心不要用过时的数据覆盖 Gantt 的内部状态。
更多细节请参阅 Gantt 作为真相来源。
配置与 Props
React 封装器接收 config prop(映射到 gantt.config)以及 templates prop(映射到 gantt.templates)。
<ReactGantt
tasks={tasks}
links={links}
config= {{
scales: [
{ unit: "year", step: 1, format: "%Y" },
{ unit: "month", step: 1, format: "%F, %Y" },
{ unit: "day", step: 1, format: "%d %M" },
],
columns: [
{ name: "text", tree: true, width: "*", resize: true },
{ name: "start_date", align: "center", resize: true },
{ name: "duration", align: "center", resize: true },
{
name: "custom",
align: "center",
template: (task) => <AlertButton task={task} onClick={handleButtonClick} />,
resize: true,
},
{ name: "add", width: 44 },
],
}}
templates= {{
task_text: (start, end, task) => `#${task.id}: ${task.text}`,
task_class: (start, end, task) => {
return task.priority === 'high' ? 'highlight-task' : '';
},
}}
/>
在模板中使用 React 组件
在属性中指定模板时,你可以从模板函数返回 React 元素:
function PriorityBadge({ priority }) {
return <span style={{color: 'red' }}>{priority}</span>;
}
<ReactGantt
templates={{
task_text: (start, end, task) => {
return <PriorityBadge priority={task.priority} />;
}
}}
/>
内部而言,DHTMLX Gantt 以非 React 的方式操作 DOM。当你从模板返回 React 组件时,它们通过门户(portals)嵌入到 Gantt 的 HTML 中。请记住,对于大型数据集,呈现大量的复杂 React 组件可能影响性能。
你可以通过模板覆盖许多方面:
- task_text、task_class 用于柱形条
- 给时间刻度头能“格式化”的刻度格式化
- 左侧网格单元的列模板
- 以及更多。请参考 Gantt 的 可用指南
你可以在以下文章中找到 React Gantt 支持的全部 prop 列表:Configuring props
主题与样式
Gantt 附带了多种内置主题,可以通过 theme prop 启用并动态切换:
import { useEffect, useRef } from 'react';
import ReactGantt from "@dhx/react-gantt";
import "@dhx/react-gantt/dist/react-gantt.css";
export default function BasicInitDemo() {
const [theme, setTheme] = useState("terrace");
const tasks = [...];
const links = [...];
const switchTheme = () => {
setTheme((prevTheme) => (prevTheme === "terrace" ? "dark" : "terrace"));
};
return (
<div style={{height: '600px'}}>
<div>
<button onClick={switchTheme}>切换主题</button>
</div>
<ReactGantt
tasks={tasks}
links={links}
theme={theme} /*!*/
/>
</div>
);
}
你可以在 此文章 中找到现有主题的详细描述。
主题还可以通过自定义样式和覆盖 CSS 变量来进一步定制:
:root {
--dhx-gantt-task-background: #d96c49;
--dhx-gantt-task-color: #fff;
--dhx-gantt-task-border-radius: 8px;
}
如需更多配置,请查看 Skins Customization 指南。
替换 Lightbox
DHTMLX Gantt 自带一个内置的可配置任务编辑器,称为 Lightbox。
如有需要,你可以通过以下任一方式将其替换为基于 React 的模态框或其他组件:
通过 customLightbox 属性提供自定义组件
要实现,请通过 customLightbox prop 传递一个组件:
import React, { useState } from 'react';
export interface CustomLightboxProps {
data: any;
onSave: (task: any) => void;
onCancel: () => void;
onDelete: () => void;
}
const CustomLightbox: React.FC<CustomLightboxProps> = ({
data,
onSave,
onCancel,
onDelete
}) => {
const [description, setDescription] = useState<string>(data.text || '');
const handleSaveClick = () => {
onSave({ ...data, text: description });
};
const modalStyles = {
...
};
return (
<div>
<div style={modalStyles.overlay} onClick={onCancel} />
<div style={modalStyles.content}>
<h3>Edit Task</h3>
<div>
<label>Description:</label>
<input
type="text"
value={description}
onChange={(e) =/> setDescription(e.target.value)}
style={{width: '100%', padding: '8px', marginTop: '10px' }}
/>
</div>
<div style={modalStyles.buttonGroup}>
<button onClick={handleSaveClick}>Save</button>
<button onClick={onCancel}>Cancel</button>
<button onClick={onDelete}>Delete</button>
</div>
</div>
</div>
);
};
export default CustomLightbox;
之后,你可以在如下方式使用新增的组件:
import { useEffect, useRef } from 'react';
import ReactGantt from "@dhx/react-gantt";
import "@dhx/react-gantt/dist/react-gantt.css";
import CustomLightbox from "./EditorModal";
export default function BasicInitDemo() {
const ganttRef = useRef(null);
const tasks = [...];
const links = [...];
useEffect(() => {
//const gantt = ganttRef.current?.instance;
}, []);
return (
<ReactGantt
ref={ganttRef}
tasks={tasks}
links={links}
customLightbox={<CustomLightbox />} />
);
}
使用 onBeforeLightbox 事件属性
对于更复杂的场景,你可以捕捉 onBeforeLightbox 事件(Lightbox 被调用时触发)并覆盖默认行为:
import { useEffect, useRef } from 'react';
import ReactGantt from "@dhx/react-gantt";
import "@dhx/react-gantt/dist/react-gantt.css";
import { useNavigate } from 'react-router-dom';
export default function BasicInitDemo() {
const ganttRef = useRef<any>(null);
const tasks = [...];
const links = [...];
const navigate = useNavigate();
const handleTaskEdit = (id: any) => {
const ganttInstance = ganttRef.current?.instance;
navigate(`/editor/${id}`, { state: { task: ganttInstance.getTask(id) } });
};
return (
<ReactGantt
ref={ganttRef}
tasks={tasks}
links={links}
onBeforeLightbox={handleTaskEdit} />
);
}
使用 JS Gantt API
请参考 Custom Lightbox 以获取覆盖或扩展内置 Lightbox 的更多细节。
替换内置模态框
默认 UI 包含两个模态弹窗:
- 删除任务前出现的确认对话框
- 删除链接前出现的确认对话框
两者都可以通过 ReactGantt 的 modals 参数进行覆盖:
<ReactGantt
...
modals={{
onBeforeTaskDelete: ({
task,
callback,
ganttInstance,
}: {
task: Task;
callback: () => void;
ganttInstance: GanttStatic;
}) => void,
onBeforeLinkDelete: ({
link,
callback,
ganttInstance,
}: {
link: Link;
callback: () => void;
ganttInstance: GanttStatic;
}) => void,
}}
...
/>
你可以使用这些参数在 Gantt 调用确认对话框时激活自定义模态框。调用参数中的 callback() 将最终完成相应任务或链接的删除。若要取消删除,只需在不调用回调的情况下关闭模态框。
在网格中使用 React 组件
在表头中
网格列的 label 属性可以是一个 string 或一个 ReactElement。这让你直接在列头中嵌入基于 React 的过滤器、按钮或其他 UI:
const config: GanttConfig = {
columns: [
{ name: "text", label: "Name", tree: true, width: 180,
resize: true },
// 直接嵌入 React 元素
{ name: "start_date", label: <DateFilter />, width: 150,
align: "center", resize: true },
// 或使用返回 React 元素的函数:
{ name: "end_date", label: () => <DateFilter />, width: 150,
align: "center", resize: true },
...
],
row_height: 40,
grid_width: 550,
};
当封装器在标签或任何其他模板属性中检测到 React 元素时,它会使用 React Portal 将该元素渲染到网格头单元格中。
在单元格中
网格单元格由列的 template 属性定义。该模板函数接收一个任务对象,必须返回一个纯字符串或一个 ReactElement:
import { useRef } from 'react';
function AlertButton({ task, onClick }) {
return <button onClick={onClick}>{`Task ID: ${task.id}`}</button>;
}
export default function GanttWithGridCells({ handleButtonClick, ganttRef }) {
const config = {
columns: [
{ name: "text", tree: true, width: 180, resize: true },
{ name: "start_date", width: 150, align: "center", resize: true },
{ name: "duration", width: 80, align: "center", resize: true },
{
name: "custom",
align: "center",
label: <span>My Column</span>,
width: 140,
// 返回 React 元素
template: (task) => (
<AlertButton
task={task}
onClick={() => {
handleButtonClick(task);
// 如有需要,强制重新渲染该任务
ganttRef.current?.instance.updateTask(task.id);
}}
/>
),
resize: true,
},
{ name: "add", width: 44 },
],
row_height: 40,
grid_width: 550,
};
return <ReactGantt ref={ganttRef} config={config} /* ...other props */ />;
}
通过从列模板返回一个 React 元素,你可以在 Gantt 网格的每个单元格中创建完全交互的内容(按钮、下拉框、徽标等)。内部,封装器会通过门户将这些元素注入到 Gantt 管理的 DOM 节点中。
在内联编辑器中
DHTMLX Gantt 支持网格单元格的 Inline Editing。在这个 React 封装中,你可以通过在 column 配置中指定一个编辑器对象,然后在 inlineEditors prop 中将编辑器名称映射到一个 React 组件来提供你自己的自定义编辑器。请看下面的示例。
定义一个基于 React 的内联编辑器组件:
import React, {
useState,
forwardRef,
useImperativeHandle
} from 'react';
import { InlineEditorMethods, InlineEditorProps } from '@dhx/react-gantt';
const MyInlineEditor = forwardRef<InlineEditorMethods, InlineEditorProps>(
({ initialValue, task, save, cancel, ganttInstance }, ref) => {
const [value, setValue] = useState(initialValue || "");
useImperativeHandle(ref, (): InlineEditorMethods => ({
getValue: () => value,
setValue: (val: any) => setValue(val),
isValid: () => true,
focus: () => {
},
isChanged: (originalValue: any) => {
return originalValue !== value;
},
save: () => { }
}));
return (
<input
type="text"
value={value}
onChange={e =/> setValue(e.target.value)}
autoFocus
/>
);
}
);
export default MyInlineEditor;
在你的 Gantt 配置中使用自定义编辑器:
import ReactGantt from "@dhx/react-gantt";
import MyInlineEditor from "./CustomInlineEditor";
function Demo() {
const config = {
columns: [
{ name: "text", tree: true, width: 180, resize: true },
{
name: "duration",
width: 80,
align: "center",
editor: { type: "customInputEditor", map_to: "text" }, /*!*/
resize: true
},
{ name: "start_date", width: 150 },
{ name: "add", width: 44 }
]
};
return (
<ReactGantt
config={config}
inlineEditors={{
customInputEditor: MyInlineEditor /*!*/
} }
tasks={[/*...*/]}
links={[/*...*/]}
/>
);
}
当用户双击列单元格时,Gantt 将在原地显示你的编辑器组件。封装器内部的代码会调用你通过 useImperativeHandle(ref, ...) 暴露的方法(如 getValue、setValue 等),确保 Gantt 实例与组件中的变更保持同步。
编辑器对象的 type 值必须与 inlineEditors 中的键对应。
map_to 属性指定编辑器将读取并写入 Task 对象中的哪一个属性。请参阅覆盖或扩展内置 Lightbox 的相关文章以获取进一步细节。
如果你实现一个编辑器,使其不仅仅是向任务对象的某个属性写入值,而是执行更复杂的逻辑,则需要在 save 函数中实现必需的逻辑,并将输入的 map_to 选项设为 "auto"。在这种情况下,Gantt 不会修改任务对象,而是在需要应用编辑器更改时调用 save 函数。编辑器的 initialValue 将作为 null 传递。
你可以通过 config 属性的 editor_types 来定义非 React 的内联编辑器。