Using React with URSYS - dsriseah/ursys GitHub Wiki

URSYS WebApps try to keep data- and app-level operations code out of the user interface, so our code can be run without an GUI or be easily adapted to other GUI frameworks. Here's an example of how a React root component interfaces with URSYS lifecycle and appcore modules.

Note

This a developing section, particularly with regard to Functional Components and URSYS.

Class Components in React

longer pseudocode example - class component

Much boilerplate stuff removed for clarity.

// DevWizard.jsx

/** IMPORT LIBRARIES **/

import React from 'react';
import UR from '@gemstep/ursys/client';
import * as WIZCORE from 'modules/appcore/ac-wizcore';
import * as DC_PROJECTS from 'modules/datacore/dc-projects';
import { ButtonConsole } from './wiz/edit/ButtonConsole';

/** lifecycle hook **/

UR.HookPhase('UR/LOAD_ASSETS', () => {
  return DC_PROJECTS(`/assets/projectname`);
});

/** react root **/

class DevWizard extends React.Component {
  constructor() {
    this.state = WIZCORE.State();
  }

  componentDidMount() {
      UR.SystemAppConfig({ autoRun: true }); // initialize renderer
      document.addEventListener('click', WIZCORE.DispatchClick);
      WIZCORE.SubscribeState(this.handleWizUpdate);
  }

  componentWillUnmount() {
    WIZCORE.UnsubscribeState(this.handleWizUpdate);
  }

  /** handle incoming WIZCORE state updates **/

  handleWizUpdate(vmStateEvent) {
    this.setState(vmStateEvent);
  }

  /** react render function **/

  render() {
    const { sel_linenum, sel_linepos, script_page } = this.state;
    return (
      <div id="gui-wizard">
        <ScriptViewWiz_Block script_page={script_page} sel_linenum={sel_linenum} />
        <ButtonConsole />
      </div>
    );
  }
}

/** component export **/

export default DevWizard;

React components construct only after React is invoked. In GEMSTEP, this happens after LOAD_ASSETS fires. Note that because the PhaseMachine hook UR/LOAD_ASSETS is defined at the time of module load, it's able to fire before React initializes. This order-of-operations is handled by application bootstrap code that controls the application lifecycle.

In React class components, the constructor is used for first initialization of state and data structures. Instead of using code like this.state = { stuff }, we're instead fetching the initial state from the WIZCORE appcore module.

  constructor() {
    this.state = WIZCORE.State();
  }

The WIZCORE appcore is using AppStateMgr to initialize state with this code:

// ac-wizcore.ts

/** import class **/

import UR from '@gemstep/ursys/client';
const { StateMgr } = UR.class;

/** create StateMgr instance **/

const STORE = new StateMgr('ScriptWizard');
const { State, SendState, SubscribeState } = STORE;

/** initialize StateMgr for use with React component constructors **/

STORE._initializeState({
  script_page: [], // source tokens 'printed' as lines
  sel_linenum: -1, // selected line of wizard. If < 0 it is not set, pointer to script_page
  sel_linepos: -1, // select index into line. If < 0 it is not set
});

...

/** export StateMgr's needed messages **/

export { State, SendState, SubscribeState };

The State() method returns the state object (which is React-compatible).

AppCore-to-Component State Loop

Because we're using WIZCORE's StateMgr class instance to manage our state, we have to create a bidirectional link.

  /** appcore-to-reactstate integration **/

  componentDidMount() {
      UR.SystemAppConfig({ autoRun: true }); // initialize renderer
      WIZCORE.SubscribeState(this.handleWizUpdate);
  }

React's componentDidMount() will fire after this component has been fully rendered, and since it's the root view that means that all its subviews have rendered as well. There are a couple things shown here:

  1. A call to UR.SystemAppConfig() tells the lifecycle system to start running the other phases, which are APP_CONFIGURE', 'APP_READY', and 'APP_START'. That means as soon as the application has completely initialized, we can make assumptions about what is available in the system.
  2. The WIZCORE.SubscribeState(this.handleWizUpdate) call receives notification from WIZCORE's state manager instance, which fires whenever that state updates. Since multiple components can import WIZCORE, that means a change made by another component will be propagated to all subscribers.

In this example, handleWizUpdate(vmStateEvent) just uses React's this.setState() directly without any filtering or processing.

  handleWizUpdate(vmStateEvent) {
    this.setState(vmStateEvent);
  }

This will of course cause the render() function to be invoked by React, passing props down to the <ScriptView> child component so it knows what to render.

  /** react render function **/

  render() {
    const { sel_linenum, sel_linepos, script_page } = this.state;
    return (
      <div id="gui-wizard">
        <ScriptView script_page={script_page} sel_linenum={sel_linenum} />
        <ButtonConsole />
      </div>
    );
  }

So that's how state is initialized in the appcore and read by the component. Now let's talk about the reverse direction.

Component-to-AppCore State Loop

React is responsible for capturing user-interface events and also updating its display state, and loops through WIZCORE's export SendState() method instead of using this.setState() in the component. This eventually fires the handleWizUpdate() method of the component which then calls this.setState().

Here's a slight modification of the sample code to include a textarea and a submit button with event handlers:

pseudocode

  /** local UI handlers **/

  updateText(event) {
    const script_text = event.target.value;
    WIZCORE.SendState({ script_text });
    // render() function will get updated via this.handleWizEvent()
  }

  submitText(event) {
    const { script_text } = this.state;
    WIZCORE.SaveScriptText(script_text);
  }

  /** render function with events **/

  render() {
    const { sel_linenum, sel_linepos, script_page } = this.state;
    const { script_text } = this.state;
    return (
      <div id="gui-wizard">
        <ScriptViewWiz_Block script_page={script_page} sel_linenum={sel_linenum} />
        <ButtonConsole />
        <textarea rows={20} value={script_text} onChange=event=>this.updateText(event) />
        <button type="submit" onClick=event=>this.submitText(event)>Submit Text</button>
      </div>
    );
  }

We've added the onChange and onClick event handlers, which are bound to arrow functions that accept the event object and pass it on to local handlers. These are doing different things, as evidenced by the way WIZCORE is being used.

  • The responsibility of the textarea onChange handler is only to updating its appearance as the user types. It does not change application data. This handler is fired when a change occurs, and we yoink the text from event.target. Using React's terminology, this is a controlled component as opposed to an uncontrolled one.
  • For the onSubmit handler of the button, we want to change application data only, so we grab the text value not out of the event.target (which is the button, not the text area), but from this.state. It's up-to-date from the prior onChange handling. we submit it to a WIZCORE SaveScriptText() method that performs the desired operation. Because WIZCORE knows what dependent state might be generated, the logic can assemble a larger state update to notify all state subscribers (e.g. a word count).

Shared StateMgr across Multiple AppCores

A component can include multiple AppCore modules, each with their own managed instance of StateMgr. However, you can also use the StateMgr's static method GetStateManager(groupName: string): StateMgr to request the instance that any given AppCore may be using so you can observe state changes in a read-only manner. This helps eliminate excessive props passing in React component trees.




Functional Components in React

Functional Components interact with URSYS through the useEffect() hook, subscribing and unsubscribing to message/state handlers. The structure of declaring useEffect() pseudocode looks like this

import React, { useState, useEffect } from 'react';

import { Endpoint } from '@ursys/core'; // this is pseudocode
const UDATA = new Endpoint();

function MyComponent ( props ) {

  // initialize state
  const [state, setState] = useState({ ... });
  
  // add "side-effect" hooks
  useEffect(() => {

    // define helpers
    const urstate_UpdateCommentVObjs = () => { ... code };
    const urmsg_UpdatePermissions = () => { ... code }

    // attach UNISYS state change and message handlers on mount
    UDATA.OnAppStateChange('COMMENTVOBJS', urstate_UpdateCommentVObjs);
    UDATA.HandleMessage('COMMENT_UPDATE_PERMISSIONS', urmsg_UpdatePermissions);

    // detach UNISYS state change and message handler on unmount
    return () => {
      UDATA.AppStateChangeOff('COMMENTVOBJS', urstate_UpdateCommentVObjs);
      UDATA.UnhandleMessage('COMMENT_UPDATE_PERMISSIONS', urmsg_UpdatePermissions);
    };
  }, [state.uIsBeingEdited]); // optional state to monitor for mount/unmount code

  // render 
  return (
    <>
      <p>{state.textTitle}</p>
    </>
  )

} // end MyComponent()

If you're not familiar with functional components, see React.dev which uses them for all its examples; class components are a legacy method for creating components that is still supported, but no longer the meta. You can intermix the types however.

longer pseudocode example - functional component

Based on older functional component in NetCreate-ITEST, but this shows the basic idea

/*//////////////////////////////// ABOUT \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\*\

  URComment is a representation of an individual comment, used in the context
  of a URCommentThread.

\*\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ * //////////////////////////////////////*/

import React, { useState, useEffect } from 'react';
import UR from '@ursys/core';
import CMTMGR from 'comment-mgr';

/// CONSTANTS & DECLARATIONS //////////////////////////////////////////////////
/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
const UDATAOwner = { name: 'URComment' };
const UDATA = UR.NewDataLink(UDATAOwner); // this is not actual UR syntax but
                                          // this gives you an idea

/// REACT FUNCTIONAL COMPONENT ////////////////////////////////////////////////
/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
/** URComment renders a single comment in the context of a URCommentThread
 *  manager.
 *  @param {string} cref - Collection reference
 *  @param {string} cid - Comment ID
 *  @param {string} uid - User ID
 *  @returns {React.Component} - URComment
 */
function URComment({ cref, cid, uid }) {
  const [state, setState] = useState({
    commenter: '',
    createtime_string: '',
    ...
  });

  /** Component Effect - initial comment viewobject on mount */
  useEffect(
    () => c_LoadCommentVObj(),
    [] // run once because no dependencies are declared
  );

  /** Component Effect - updated comment */
  useEffect(() => {
    // declare helpers
    const urmsg_UpdatePermissions = data => {
      setState(prevState => ({
        ...prevState,
        uIsDisabled: data.commentBeingEditedByMe
      }));
    };
    const urstate_UpdateCommentVObjs = () => c_LoadCommentVObj();

    // hook UNISYS state change and message handlers
    UDATA.OnAppStateChange('COMMENTVOBJS', urstate_UpdateCommentVObjs);
    UDATA.HandleMessage('COMMENT_UPDATE_PERMISSIONS', urmsg_UpdatePermissions);

    // cleanup methods for functional component unmount
    return () => {
      if (state.uIsBeingEdited) CMTMGR.UnlockComment(state.cid);
      UDATA.AppStateChangeOff('COMMENTVOBJS', urstate_UpdateCommentVObjs);
      UDATA.UnhandleMessage('COMMENT_UPDATE_PERMISSIONS', urmsg_UpdatePermissions);
    };
  }, [state.uIsBeingEdited]); // run when uIsBeingEdited changes

  /// COMPONENT HELPER METHODS ////////////////////////////////////////////////
  /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  /** Declare helper method to load viewdata from comment manager into the
   *  component state */
  function c_LoadCommentVObj() {
    ... 
    // set loaded component state
    setState({ ... stuff here });
  }

  /// COMPONENT UI HANDLERS ///////////////////////////////////////////////////
  /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  /** handle edit button, which toggles the viewmode of this URComment */
  const evt_EditBtn = () => { ... code here };
  /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  /** handle save button, which saves the state to comment manager.
   *  looks like there are some side effects being handled at the end */
  const evt_SaveBtn = () => { ... code here };
  /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  /** handle select button, which updates the comment type associated with this
   *  comment via comment manager */
  const evt_TypeSelector = event => { ... } 
  /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  /** handle input update, which updates the text associated with this comment
   *  via comment manager */
  const evt_CommentText = (index, event) => { ... }

  /// COMPONENT RENDER ////////////////////////////////////////////////////////
  /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  /** The URComment conditionally renders its display state based on several
   *  states:
   *  - isAdmin - read from commentmgr directly
   *  - state.uViewMode - "edit" or "view" mode of comment
   *  - state.uIsSelected - is current selected comment
   *  - etc...
   */
  const {
    commenter,
    selected_comment_type,
    ... more state stuff
  } = state;

  const isAdmin = CMTMGR.IsAdmin();
  const comment = CMTMGR.GetComment(cid);
  const commentTypes = CMTMGR.GetCommentTypes();

  /** declare subcomponents **/
  const EditBtn = <button className="outline small" onClick={evt_EditBtn}>Edit</button>;
  const SaveBtn = <button onClick={evt_SaveBtn}>Save</button>;
  const TypeSelector = (
    <select value={selected_comment_type} onChange={evt_TypeSelector}>
      {[...commentTypes.entries()].map(type => (
        <option key={type[0]} value={type[0]}>
          {type[1].label}
        </option>
      ))}
    </select>
  );

  /** return renderable jsx for component **/
  return(
    <>
      <div className="editbar">
        {EditBtn}
        {SaveBtn}
      </div>
      <div>{TypeSelector}</div>
    </>
  );
}

/// EXPORT REACT COMPONENT ////////////////////////////////////////////////////
/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
export default URComment;




Appendix: Lifecycle Notes

URSYS controls its lifecycle from application load to start with Phase Hooks. One of the most used ones in any module is to tap into a LOAD_ASSETS that happens very early in system initialization, even before React renders anything.

/** lifecycle hook **/

UR.HookPhase('UR/LOAD_ASSETS', () => {
  return DC_PROJECTS(`/assets/projectname`);
});

Here, it's using the UR phasemachine, which in GEMSTEP controls the app's overall lifecycle. As a quick synopsis, the lifecycle stages for the UR PhaseMachine are something like this:

'SYS_INIT',     // initialize key runtime parameters
'DOM_READY',    // the dom is stable
'NET_CONNECT',  // initiate connection to URNET message broker
'NET_READY',    // the network is stable with initial services
'LOAD_DB',      // load database stuff from wherever
'LOAD_CONFIG',  // app modules can request asynchronous loads
'LOAD_ASSETS'   // can use loaded configs to load assets
'APP_CONFIGURE' // app modules can configure data structure from loaded data
'APP_READY'     // all apps have loaded and configured and are ready to run
'APP_START',    // app modules start execution, all modules are ready




Appendix: Handling State Outside of React Components

URSYS is designed to work independently of React's many obtuse implementations of application state (e.g. redux, context, higher-order components), as we try to maintain UI Framework Independence. Typically, we use URSYS lifecycle hooks to allow modules to implement datastores with pub/sub methods and direct invocation hooks.

For both functional and class components, we provide subscribe/unsubscribe methods for a State Manager (exposed through "AppCore" modules) that fire during an appropriat lifecycle (either on component mount or on hooks to PhaseMachine, as well as unhooking).

⚠️ **GitHub.com Fallback** ⚠️