Our pure JavaScript Scheduler component


Post by Rama »

Dear support,

Just for an example consider following use case -

StateValue1.PNG
StateValue1.PNG (45.02 KiB) Viewed 1559 times

In above screen as indicated - I put some value in student name textbox. The value entered in this textbox is saved in the component state. I click "Show name of student". Inside event handler I just display the value of state and it shows the right value.

Now I do following steps

    • Select start end which is less than end date
    • Click Get button
    • Scheduler shows project and module
    • Click Add Event inside the scheduler
    • Enter event details
    • Click Save
    • Inside click handler I am trying to access the name of student from state but I don't get any value
StateValue2.PNG
StateValue2.PNG (52.35 KiB) Viewed 1559 times

Why is that event handler of scheduler control does not get any state values of the component in which it is present ?

I have attached zip file containing source code.

Attachments
code.zip
(3.28 MiB) Downloaded 128 times

Post by Rama »

Dear Support,

Can you suggest what we might be doing wrong or some workaround for this issue ?


Post by saki »

Actually, this is more React question than Scheduler question because the problem you are facing is cause by how the hook useState and React functional components work.

What is happening is that when the Scheduler is created the listener functions are create together with their context at the time of creation. That means that each listener function will have an access to nameOfStudent but only to its initial value what is empty string in your case.

The solution is useContext instead of useState because context will be saved on the upper level (for example in App.tsx) and will be made available by lower level components.

Your App.tsx may look like:

import React, { createContext } from 'react';
import './App.css';
import ProjectUserScheduleContainer from './Container';

const globalData = {
  nameOfStudent: 'context initial name'
};

export const globalContext = createContext(globalData);

function App() {
  return (
    <globalContext.Provider value={globalData}>
      <div className="App">
        <ProjectUserScheduleContainer />
      </div>
    </globalContext.Provider>
  );
}

export default App;

and the beginning of ProjectUserScheduleViewer.tsx like:

import React, { useRef, useState, useEffect, useContext } from "react";
import { globalContext} from './App';

export default function ProjectUserScheduleViewer(props: IProps) {
    const globalData = useContext(globalContext)
    // ...

You would then replace nameOfStudent with globalData.nameOfStudent in the rest of the code.

See the attached workable code with the above solution implemented.

Attachments
code.zip
(3.15 MiB) Downloaded 106 times

Post by Rama »

Dear Support,

While solution suggested by you works. I am still unclear as to why a normal button click event handler would get state value and why scheduler event handler not get state value.

Are you saying that we should use Scheduler with Class based component instead of functional components ?


Post by saki »

Hello Rama,

what is actually happening is that with Scheduler we work the other way how React is designed to work. React watches changes in props and state and re-renders its content (html markup) , including re-generating functions, as needed of its own volition. However, we do not want Scheduler (its markup) to be destroyed from outside so we prevent it from being re-rendered by React. Then it runs in "old context" so to say and it can only access the original state.

Consider the following code:

import React, { useRef, useEffect, useState } from 'react';
import { Scheduler } from 'bryntum-scheduler';

import './App.css';

function App() {
  const [text, setText] = useState('Initial Text');
  const schedulerRef = useRef();

  useEffect(() => {
    const handler = () => {
      console.log(`The text is: ${text}`);
    };
    console.log(`Running useEffect()`);
    // prettier-ignore
    schedulerRef.current = new Scheduler({
        adopt: 'content',
        listeners: {
          mouseOver: handler
        }
      });
  }, [text]);
  return (
    <>
      <button onClick={() => setText('Other text')}>
        Change text
      </button>
      <div id="content"></div>
    </>
  );
}

export default App;

The above works from the React viewpoint and it behaves as you would expect. However, it creates new Scheduler whenever the text state variable changes. The existing scheduler is not properly destroyed because we would need to call its destroy() method. That we could do before creating the new one but then we would lose state (scroll position, zoom state, perhaps some data too).

The solution is apparently easy, let's make the test if the scheduler already exists and create the new one only when it doesn't:

    // ...
    schedulerRef.current = schedulerRef.current || new Scheduler({
    // ...

Now, scheduler is preserved but our listener always logs Initial Text.

I hope this answers your first question.

To the second: No we do not suggest class-based design of your app. We use class-based wrappers only because of backwards compatibility with React versions that did not support hooks. Use hooks freely, only remember that if you need to access data from the local state in Scheduler listeners or renderers you need useContext (or Redux, or another approach).


Post by Rama »

I now understand. Thank you so much for this explanation.


Post Reply