search envelope-o feed check
Home Unanswered Active Tags New Question
user comment-o

React Scheduler > Uncaught Error when moving resource after collapse

Asked by Andy
3 days ago.

Objective: I have a deeply nested tree of resources. One of the requirement is to collapse all the siblings of the moved resource to hide the children of the siblings for user to see. We decide to handle the collapse state ourselves. This is a rough snippet.

const [collapseState, setCollapseState] = useState({});

const activeMovedRowIdRef = useRef(null);

const collapseSiblings = (rowId) => { 
 setCollapseState(...) // just collapse sibilings
}

const resources = rows.map((row) => ({
 ...row
 expanded: !collapseState[row.id]
});

onRowMoving(args){
 if(activeMovedRowIdRef.current !== args.row.id) {
  collapseSiblings(args.row.id);
  activeMovedRowIdRef.current = args.row.id;
 }

 const { nextPosition } = validatePosition(args);

 args.position = nextPosition;
} 

onRowMove(args){
 const { nextPosition } = validatePosition(args);

 if (nextPosition === 'forbidden') {
        args.preventDefault(); // Revert back to original position
        return;
  }

 args.position = nextPosition;
}

Bug: While the resource tree’s UI re-renders correctly to show the collapsed children, when I drop the moved row (`onRowMove` will be called), I get the below error. The error also shows even if there is no change in the collapse state (ie: none of the siblings are expanded, but `collapseSiblings` is called)

Can you provide guidance? I attached the error as code snippet since I can’t see the screenshot in the preview.

helpers.js:112 

Uncaught Error: Cannot find source node parent
    at new push.192648.DayPilot.Exception (daypilot-react.min.js:10:1)
    at M.vt (daypilot-react.min.js:34:1)
    at push.192648.DayPilot.Scheduler.st (daypilot-react.min.js:34:1)
    at push.192648.n.Hg (daypilot-react.min.js:39:1)
    at HTMLDocument.sentryWrapped (helpers.js:90:1)
push.192648.DayPilot.Exception	@	daypilot-react.min.js:10
M.vt	@	daypilot-react.min.js:34
st	@	daypilot-react.min.js:34
push.192648.n.Hg	@	daypilot-react.min.js:39
sentryWrapped	@	helpers.js:90

Comment posted by Dan Letecky [DayPilot]
2 days ago.

This happens because you change the resources array in onRowMoving.

Updating the Scheduler in onRowMoving is not a safe operation - it’s not guaranteed to work. In this case, the Scheduler can’t find the tree node in the updated hierarchy.

Please let me check how best to support this scenario.

Answer posted by Dan Letecky [DayPilot]
2 days ago.

The latest sandbox build (2025.3.6659) now supports rows.collapse() method and onRowMoveStart event.

You can use them to implement this functionality:

onRowMoveStart: args => {
    scheduler.rows.collapse(r => r.parent()?.id === args.row.parent().id);
},
Comment posted by Andy
1 day ago.

Hi Dan,

Thank you for the response.

I installed the latest sandbox build `https://npm.daypilot.org/daypilot-pro-react/trial/2025.4.6661.tar.gz` and tried triggering both `scheduler.rows.collapse` and `setCollapseState`.

Unfortunately, the error is still thrown. This time, however, the resource left pane experienced a brief flash that previously didn’t occur without calling `scheduler.rows.collapse`.

onRowMoving: args => {
    scheduler.rows.collapse(r => r.parent()?.id === args.row.parent().id);
    setCollapseState(...) // just collapse sibilings with React state

    ^ Both of these 
},

I am intentionally storing and managing own collapse state instead of relying on Daypilot collapse tree due to a technical design decision.

Can you provide additional guidance?

Comment posted by Dan Letecky [DayPilot]
1 day ago.

Andy,

Thanks for the update.

> I am intentionally storing and managing own collapse state instead of relying on Daypilot collapse tree due to a technical design decision.

Can you please share details?

I’m asking because calling rows.collapse() updates the tree state in-place (i.e. it modifies scheduler.resources array, without triggering any state change notification).

Modifying resources triggers yet another update, that’s why you see the flicker.

The drag and drop operations are not designed to survive an update of the Scheduler. Your code invokes an update by creating a new copy of resources dynamically during row moving. To avoid this problem, this new row.collapse() method implements a safe update of the tree state.

If you need to store the tree state, one of the options is to simply make a copy of scheduler.resources (or extract the necessary data from there).

> Unfortunately, I think it is only partial solution, but doesn’t resolve the problem in which `onRowMoveEnd` will not be called if we move the resource row outside of the valid drop zone (ie: the resource pane), meaning `activeMovedRowIdRef.current` won’t be reset to `null`.

Yes, you are right - it won’t be reset. I tried to suggest a solution that is as simple as possible. That included dropping activeMovedRowIdRef. My understanding was that it was only needed for the collapse logic. If you need it for something else as well, please let me know.

Comment posted by Andy
1 day ago.

Hi Dan,

> I am intentionally storing and managing own collapse state instead of relying on Daypilot collapse tree due to a technical design decision. Can you please share details?

We are designing a timeline framework. Our framework has shared controller code that is library-agnostic, including the collapse logic. The controller logic will be mapped to specific props at the library level. For example, we have a `getIsExpanded(id)` that can be mapped to `resource > expanded` field.

Our team has a big codebase that for some reason uses multiple libraries for timeline. At some point, we will migrate everything to 1 library that matches our use cases (hopefully it is Daypilot), but we don’t have the capacity to do so at this point.

Here is a high level design:

TimelineFramework (React Component)
 -> Controller (React hook > that stores collapse state and actions that collapse/expand etc..)
   -> Daypilot React Scheduler (gets the collapsed state and callbacks from Controller above)
   -> Another React timeline library (gets the collapsed state and callbacks from Controller above)
   -> Another React timeline library Nth (gets the collapsed state and callbacks from Controller above)

Since the controller level already provides all the necessary collapse logic, we won’t need to rely on, nor copy/extract, Daypilot’s internal collapse state.

> The drag and drop operations are not designed to survive an update of the Scheduler. Your code invokes an update by creating a new copy of resources dynamically during row moving. To avoid this problem, this new row.collapse() method implements a safe update of the tree state.

I see. I have tried both approaches:

  1. Calling only `scheduler.rows.collapse()`: The scheduler flashes but the UI isn’t reflected with the latest collapse state, assuming this is because react state isn’t updated. The row move is successful without error.

  2. Calling both `scheduler.rows.collapse()` and react state update setter: UI is reflected with the latest collapse state, but shows error (as seen the screen recording above) when dropping the row.

I am still looking for a solution where the UI gets reflected with the latest collapse state and row is successfully moved without throwing an error. Maybe a `onRowMoveBegin` as described below might work.

> Yes, you are right - it won’t be reset. I tried to suggest a solution that is as simple as possible. That included dropping activeMovedRowIdRef. My understanding was that it was only needed for the collapse logic. If you need it for something else as well, please let me know.

For sure. If we can’t reset activeMovedRowIdRef once the row move session ends, the immediate next time the same row gets moved, activeMovedRowIdRef will contain the id of this row, meaning the collapse siblings call won’t be triggered.

if(activeMovedRowIdRef.current !== args.row.id) { // -> The id is the same

  collapseSiblings(args.row.id); // -> This won't be called because
  activeMovedRowIdRef is never reset.
  
activeMovedRowIdRef.current = args.row.id;
 }

Our drag and drop flow is as follows:

  1. User presses and hold onto the remove move handler/icon

  2. Because `onRowMoving` is being called continuously as position changes, we keep track of `activeMovedRowIdRef`.

    - If it’s a new row move session, we collapse its siblings and assign `activeMovedRowIdRef` the current row’s id.

    - This is to avoid continuously calling `collapseSiblings` while moving the same row to a new position.

  3. Once user drops the row, reset `activeMovedRowIdRef` inside `onRowMove`. This step won’t be completed if user drops outside of the scheduler.

———

Bottom line is we are unable detect when a row move session begins with just `onRowMoving`.

I am looking for something like a `onRowMoveBegin` that is triggered only once before the move session truly begins, and it should update the Scheduler, if necessary, before starting the row move session.

// Called (before `onRowMoving`?)
onRowMoveBegin(args){
 // No need to track `activeMovedRowIdRef`

 // Scheduler updates state before beginning a new row move session
 setCollapseState(args.row.id); 
}

Our product deals with potentially very deeply nested tree with hundreds of resources, so collapsing sibilings before moving the row is required for UX enhancement purpose.

Comment posted by Dan Letecky [DayPilot]
1 day ago.

Hi Andy,

Thanks for the detailed explanation.

Just a quick first note: Maybe you have overlooked that - there is now a new onRowMoveStart event which is fired just once, as soon as row moving has started.

Please let me check how to handle the rest.

Comment posted by Andy
1 day ago.

Oh I didn’t know about it!

I was looking at this page and didn’t see it.

https://doc.daypilot.org/scheduler/row-moving/

Let me see if it works to avoid tracking activeMovedRowIdRef

Comment posted by Dan Letecky [DayPilot]
1 day ago.

This was added to support your use case (yesterday).

If you don’t care about the internal tree state being out of sync with the React state (if you don’t save it for future visits it might not be necessary), you can use this simple implementation:

onRowMoveStart: args => {
    scheduler.rows.collapse(r => r.parent()?.id === args.row.parent().id);
},
Comment posted by Andy
1 day ago.

Hi Dan,

Using `onRowMoveStart` works just fine - activeMovedRowIdRef is no longer required!

About the collapse state, just calling the above snippet without updating the React state does not update the UI.

This is what I have as a simple example.

const controlRef = useRef();

const getIsExpanded = (id) => {
 return !!ownCollapseState[id];
}

const resources = [{
  id: '1',
  isExpanded: getIsExpanded('1') // true
},
{
  id: '2',
  isExpanded: getIsExpanded('2') // true
}]

const onRowMoveStart = args => {
  controlRef.current.rows.collapse(r => r.id === '1' || r.id === '2');

  => The UI still shows the above 2 resources as expanded on the screen, not collapsed
},

<Scheduler 
resources={resources} 
controlRef{controlRef} 
onRowMoveStart{onRowMoveStart} 
/>

Comment posted by Dan Letecky [DayPilot]
1 day ago.

Hi Andy,

If you try this, does it work? It seems to work fine for me with version 2025.4.6661:

import React, {useEffect, useRef, useState} from 'react';
import {DayPilot, DayPilotScheduler} from "daypilot-pro-react";

const Scheduler = () => {

  const controlRef = useRef();

  const getIsExpanded = (id) => {
    return true;
  }

  const resources = [{
    name: "Resource 1",
    id: '1',
    expanded: getIsExpanded('1'),
    children: [
      {
        name: "Resource 1.1",
        id: '1.1',
        expanded: getIsExpanded('1.1')
      }
    ]
  },
    {
      name: "Resource 2",
      id: '2',
      expanded: getIsExpanded('2'),
      children: [
        {
          name: "Resource 2.1",
          id: '2.1',
          expanded: getIsExpanded('2.1')
        }
      ]
    }]

  const onRowMoveStart = args => {
    controlRef.current.rows.collapse(r => r.id === '1' || r.id === '2');
  };


  return (
    <div>
      <DayPilotScheduler
        resources={resources}
        controlRef={controlRef}
        onRowMoveStart={onRowMoveStart}
        treeEnabled={true}
        rowMoveHandling={"Update"}
      />
    </div>
  );
}
export default Scheduler;
Comment posted by Andy
1 day ago.

Hi Dan,

My bad. It does work - it’s just that the UI of the collapse indicator (we have an icon) on the resource rows weren’t rerendered because it was also reading from `getIsExpanded`.

I see what you mean by the internal state versus our own React state being out of sync now.

After discussing with our team, ideally, we should just simply update our own collapse state, which re-renders `resources` array, without having to do `controlRef.current.rows.collapse` in onRowMoveStart.

The challenge is the React Scheduler component would have to react to the updated resources array before move session begins, I believe.

Is this something your team able to support?

Comment posted by Dan Letecky [DayPilot]
1 day ago.

Hi Andy,

Please let me check that.

And a couple of related notes to consider:

When fine-tuning the behavior, it may not be possible to rely purely on the declarative (state-based) model. For example, when you collapse the tree nodes this way, the source row may jump out of the viewport, especially if there are many nodes. Then you may want to bring it back using scrollToResource(). This will force you to use the direct API anyway.

Also, when the number of resources becomes big, it might be necessary to enable some optimization techniques (like on-demand children loading). The direct API methods generally offer better optimization options because they clearly state the intent.

So it is possible that at some point you might need to make compromises and abandon the pure state-driven behavior.

Comment posted by Andy
1 day ago.

Hi Dan,

Thank you for the heads-up. These are all good points.

> So it is possible that at some point you might need to make compromises and abandon the pure state-driven behavior.

We truly hope we don’t have resort to this, since our app is very React heavy and doing so might deviate from our codebase pattern which might increase maintainability. We will try to find potential workarounds.

> For example, when you collapse the tree nodes this way, the source row may jump out of the viewport, especially if there are many nodes. Then you may want to bring it back using scrollToResource(). This will force you to use the direct API anyway.

True - i will try to set up some mock data and confirm with my team if this will be an issue (most likely).

In the meantime, I look forward to your response after checking with your team.

> Also, when the number of resources becomes big, it might be necessary to enable some optimization techniques (like on-demand children loading). The direct API methods generally offer better optimization options because they clearly state the intent.

We will keep this mind if we run into massive resource tree. So far, i think progressive row rendering should be sufficient.

Comment posted by Dan Letecky [DayPilot]
3 hours ago.

Hi Andy,

The latest sandbox buidl (2025.4.6664) adds support for row drops in the Scheduler when rows update mid-drag.

Could you please give it a try

Please let me know if there is any problem.

Comment posted by Andy
3 hours ago.

It works perfectly!

Thank you very much, Dan!

Comment posted by Dan Letecky [DayPilot]
1 hour ago.

Great, thanks for the update!

New Reply
This reply is
Attachments:
or drop files here
Your name (optional):