[REACT] Gantt component crash when use dispatcher to bind `viewPreset`
Hello Bryntum Team,
Please see a demo application attached. The Bryntum component crashes on view preset changes when there is a partner BryntumGantt component. Steps to reproduce:
- Open the app project and run "npm install" command"
- Run "npm start" command to lauch the application
- Click on the following buttons: "Show Partner Gantt" -> "Month" -> "Year" and see that app crashes after it.
This is a critical issue for us. Need help ASAP.
Thank you.
Perhaps it would be worth to try to reproduce it in vanilla by repeatedly assigning viewPreset to the Gantt instance. I doubt it's a bug in a wrapper.
I have re-investigated the showcase and the final conclusion is that the problem stems from partnering the gantts with one of the not-existent. The offending code from the original showcase:
{state.bottomGanttVisible && (
<div>
<BryntumGantt
tasks={state.tasks}
columns={state.columns}
viewPreset={state.viewPreset}
partner={gantt.current?.instance}
{...ganttConfig}
/>
</div>
)}
where bottomGanttVisible is initially false. This code does not create gantt if condition is not met. The correct solution is to start with the invisible but existing gantt:
{state.bottomGanttVisible && (
<div>
<BryntumGantt
tasks={state.tasks}
columns={state.columns}
viewPreset={state.viewPreset}
partner={gantt.current?.instance}
{...ganttConfig}
/>
</div>
)}
The complete code is in the attached zip. It is upgraded from react scripts to Vite. Closing the ticket.
@jsakalos This is not the reason of this issue. In your version of the code the horizontal scroll bars synchronization doesn't work (the partnership functionality). Once you add this functionality via calling "addPartner" function, you will see the same error.
Yes, unfortunately there's still a bug. We will try to reproduce it without React and subsequently fix.
I have tried to reproduce the problem in an vanilla application w/o any framework and I was successful. I have used multiple-gantt-charts demo and replaced the content of app.js with:
import '../_shared/shared.js'; // not required, our example styling etc.
import Gantt from '../../lib/Gantt/view/Gantt.js';
import '../../lib/Scheduler/column/DurationColumn.js';
import '../../lib/Gantt/column/StartDateColumn.js';
import '../../lib/Gantt/column/EndDateColumn.js';
import '../../lib/Gantt/feature/Labels.js';
import Splitter from '../../lib/Core/widget/Splitter.js';
import ViewPreset from '../../lib/Scheduler/preset/ViewPreset.js';
const columns = [
{ type : 'name', width : 250, field : 'name' },
{ type : 'startdate', field : 'startDate' },
{ type : 'enddate', field : 'endDate' },
{ type : 'duration' }
];
const tasks = [
{
id : 1,
name : 'Portfolio A',
startDate : '2025-01-01',
endDate : '2025-12-31',
manuallyScheduled : true,
children : [
{
id : 11,
name : 'Program A',
startDate : '2025-01-01',
endDate : '2025-08-01',
manuallyScheduled : true,
children : [
{
id : 111,
name : 'Project A',
startDate : '2025-01-01',
endDate : '2025-05-01',
manuallyScheduled : true
},
{
id : 112,
name : 'Project B',
startDate : '2025-05-01',
endDate : '2025-08-01',
manuallyScheduled : true
}
]
},
{
id : 12,
name : 'Program B',
startDate : '2025-04-01',
endDate : '2025-12-31',
manuallyScheduled : true,
children : [
{
id : 121,
name : 'Project A',
startDate : '2025-04-01',
endDate : '2025-10-01',
manuallyScheduled : true
},
{
id : 122,
name : 'Project B',
startDate : '2025-10-01',
endDate : '2025-12-31',
manuallyScheduled : true
}
]
}
]
},
{
id : 2,
name : 'Portfolio B',
startDate : '2025-01-01',
endDate : '2026-12-31',
manuallyScheduled : true,
children : [
{
id : 21,
name : 'Program A',
startDate : '2025-01-01',
endDate : '2026-03-01',
manuallyScheduled : true,
children : [
{
id : 211,
name : 'Project A',
startDate : '2025-01-01',
endDate : '2025-08-01',
manuallyScheduled : true
},
{
id : 212,
name : 'Project B',
startDate : '2025-08-01',
endDate : '2026-03-01',
manuallyScheduled : true
}
]
},
{
id : 22,
name : 'Program B',
startDate : '2026-03-01',
endDate : '2026-12-31',
manuallyScheduled : true,
children : [
{
id : 221,
name : 'Project A',
startDate : '2026-03-01',
endDate : '2026-11-01',
manuallyScheduled : true
},
{
id : 222,
name : 'Project B',
startDate : '2026-11-01',
endDate : '2026-12-31',
manuallyScheduled : true
}
]
}
]
}
];
const topGantt = new Gantt({
appendTo : 'container',
columns,
tasks,
viewPreset : 'manyYears',
tbar : [
{
type : 'slidetoggle',
checked : true,
text : 'Enable row dragging',
onChange({ value }) {
topGantt.features.rowReorder.disabled = bottomGantt.features.rowReorder.disabled = !value;
}
},
{
type : 'slidetoggle',
checked : true,
text : 'Sync scrolling',
cls : 'scrollToggle',
style : {
'margin-inline-start' : '1em'
},
onChange({ value }) {
bottomGantt.hideHeaders = value;
if (value) {
topGantt.addPartner(bottomGantt);
}
else {
topGantt.removePartner(bottomGantt);
}
}
},
{
type : 'button',
ref : 'zoomInButton',
cls : 'b-transparent',
icon : 'b-icon-search-plus',
tooltip : 'Zoom in',
onAction : () => topGantt.zoomIn()
},
{
type : 'button',
ref : 'zoomOutButton',
cls : 'b-transparent',
icon : 'b-icon-search-minus',
tooltip : 'Zoom out',
onAction : () => topGantt.zoomOut()
},
'->',
{
type : 'button',
text : 'Set year view',
onAction : () => {
topGantt.viewPreset = bottomGantt.viewPreset = 'manyYears';
bottomGantt.viewPreset = bottomGantt.viewPreset = 'manyYears';
}
},
{
type : 'button',
text : 'Set month view',
onAction : () => {
topGantt.viewPreset = bottomGantt.viewPreset = 'monthAndYear';
bottomGantt.viewPreset = bottomGantt.viewPreset = 'monthAndYear';
}
},
{
type : 'button',
text : 'Set visibility',
onAction : () => {
bottomGantt.hidden = !bottomGantt.hidden;
}
}
]
});
const bottomGantt = new Gantt({
appendTo : 'container',
columns,
tasks,
viewPreset : 'manyYears',
hidden : true,
partner : topGantt
});
topGantt.expandAll();
Then click in this order: Set visibility, Set month view, Set year view. The crash will result:
Being that this will not reproduce on vanilla classes, I suspect the wrapper can be improved.
If multiple properties are being updated, these should be gathered into one config object and applied using instance.setConfig(object)
In that way, the ordering of ingestion of configs will be correct. Some configs depend upon the value of others. This is handled using setConfig
this method:
function applyPropValue(configOrInstance: any, prop: string, value: any, isConfig = true): void {
// Assigning React wrapper component instance
if (value?.current?.instance) {
value = value.current.instance;
}
if (prop === 'features' && typeof value === 'object') {
Object.keys(value).forEach(key =>
applyPropValue(configOrInstance, `${key}Feature`, value[key], isConfig)
);
}
else if (prop === 'config' && typeof value === 'object') {
Object.keys(value).forEach(key =>
applyPropValue(configOrInstance, key, value[key], isConfig)
);
}
else if (prop === 'columns' && !isConfig) {
configOrInstance.columns = value;
}
else if (prop.endsWith('Feature')) {
const
{ features } = configOrInstance,
featureName = prop.replace('Feature', '');
if (isConfig) {
features[featureName] = value;
}
else {
const feature = features[featureName];
if (feature) {
feature.setConfig(value);
}
}
}
else {
configOrInstance[prop] = value;
}
}
Any forEach loop there should build a config object, and apply the changes in bulk using setConfig
Can we experiment to see how the following works?
function applyPropValue(configOrInstance: any, prop: string, value: any, isConfig = true): void {
// Assigning React wrapper component instance
if (value?.current?.instance) {
value = value.current.instance;
}
if (prop === 'features' && typeof value === 'object') {
const config = {};
Object.keys(value).forEach(key =>
applyPropValue(config, `${key}Feature`, value[key], isConfig)
);
configOrInstance.setConfig(config); // Apply all props <-----------------------
}
else if (prop === 'config' && typeof value === 'object') {
const config = {};
Object.keys(value).forEach(key =>
applyPropValue(config, key, value[key], isConfig)
);
configOrInstance.setConfig(config); // Apply all props <-----------------------
}
else if (prop === 'columns' && !isConfig) {
configOrInstance.columns = value;
}
else if (prop.endsWith('Feature')) {
const
{ features } = configOrInstance,
featureName = prop.replace('Feature', '');
if (isConfig) {
features[featureName] = value;
}
else {
const feature = features[featureName];
if (feature) {
feature.setConfig(value);
}
}
}
else {
configOrInstance[prop] = value;
}
}