Upgrade to 8.0.10 breaks components [Fixed]

We’ve upgraded our gateway to 8.0.10 which broke our components. Property changes no longer update the component. The component wil however update properties. Does this have anything to do with: https://inductiveautomation.com/downloads/releasenotes/8.0.10

Gateway - Fixed - Implemented defenses against cross-origin hijacking of the perspective websocket.

Our components import packages from other sources. Is there any way to allow properties to update our components again?

I don’t think the CORS issue is likely to be related.

I think the primary change we made for 8.0.10 was that we no longer ‘auto apply’ mobx.observer to components that get registered with the ComponentRegistry.

We used to check all components, and if they weren’t already observers, we’d wrap them. Unfortunately, update to mobx removed our ability to simply check if a component class was already an observer.

So I’d try that first: make sure the component class you register as part of your ComponentMeta is a mobx.observer. We do this by simply annotating our component with the @observer decoration:

import {observer} from 'mobx-react';

@observer
export class MyComponent extends Perspective.Component<{}, {}> {
}

4 Likes

I tryed it, it didn’t work. This is one of our components that broke, it still works in 8.0.8:

import * as React from 'react';
import {observer} from 'mobx-react';
import { Component, ComponentMeta, ComponentProps, SizeObject } from '@inductiveautomation/perspective-client';
import SignatureCanvas from 'react-signature-canvas';

import bind from 'bind-decorator';


// the 'key' or 'id' for this component type.  Component must be registered with this EXACT key in the Java side as well
// as on the client side.  In the client, this is done in the index file where we import and register through the
// ComponentRegistry provided by the perspective-client API.
export const COMPONENT_TYPE = "at-display-signaturecanvas";


// this is the shape of the properties we get from the perspective 'props' property tree-
export interface SignatureProps {
    base64Data: string;   // the url of the image this component should display
    clearCanvas: boolean;
    trimCanvas: boolean;
    penColor: string;
}

@observer
export class Signature extends Component<ComponentProps, any> {
    private sigCanvasRef = React.createRef<any>();

    @bind
    clearCanvas()
    {
        // console.log("The canvas will be cleared");

        this.sigCanvasRef.current.clear();
    }

    @bind
    handleDraw()
    {
        // console.log("The drawing has ended");

        let temp;

        if (this.props.props.read('trimCanvas'))
        {
            temp = this.sigCanvasRef.current.getTrimmedCanvas();
        }
        else
        {
            temp = this.sigCanvasRef.current.getCanvas();
        }

        const result = temp.toDataURL("image/png;base64", null);
        this.props.props.write('base64Data', result);
    }

    render() {

        if (this.props.props.read('clearCanvas'))
        {
            this.clearCanvas();

            this.props.props.write('clearCanvas', false);
            this.props.props.write('base64Data', "");
        }

        const penColor = this.props.props.read('penColor');

        return (
            <SignatureCanvas
                canvasProps={{...this.props.emit()}}
                penColor={penColor}
                onEnd={this.handleDraw}
                ref={this.sigCanvasRef}
                clearOnResize={true}
            />
        );
    }

}

// this is the actual thing that gets registered with the component registry-
export class SignatureMeta implements ComponentMeta {

    getComponentType(): string {
        return COMPONENT_TYPE;
    }

    // the class or React Type that this component provides
    getViewClass(): React.ReactType {
        return Signature;
    }

    getDefaultSize(): SizeObject {
        return ({
            width: 360,
            height: 360
        });
    }
}

Edit:
The Designer console show this when I refresh the page and the clearCanvas property has been set to true:

11:36:19.277 [Browser Events Thread] INFO Perspective.Designer.BrowserConsole - TypeError: Cannot read property ‘clear’ of null
11:36:19.277 [Browser Events Thread] INFO Perspective.Designer.BrowserConsole - ui.ErrorBoundary: Component error caught in error boundary: undefined

I’m also dealing with a similar issue in a different component I’m currently building from scratch:
TypeError: Cannot read property ‘createElement’ of null, although i’m unable to determine if this is related.

Update: I’ve determined that the TypeErrors are unrelated, fixing them did not solve this issue.

Components are able to update properties on our components in the designer but somehow that doesn’t propagate to a react state change inside the component. It’s difficult to diagnose the exact problem.

I’ve set up a react/typescript development environment that implements the toolchain and configurations inside the /web/packages/client folder in the SDK example. This way, I can seperate building the React component from building a perspective module.

It seems like the sets of rules/configurations defined in the /web/packages/client folder in the SDK example aren’t the same as the ones that the Designer and Perspective require. I’ve had it happen that a component builds inside of my dev environment, succesfully builds with the gradle buildtool inside the module, but still produce typescript related errors inside the console.

Is the current SDK Example still up to date?

1 Like

Did you ever find a fix to this @bas.dejong?

Hi @kgamble,

I haven’t heard from bas recently, so I’m assuming things are ok. I was able to confirm that the public examples seem to be working and loading as expected for me, but did push an update to make sure the Image component has mobx-react @observable applied.

As far as I am currently aware, things are working as expected. Let me know if that’s not the case fo you.

Thanks for checking in Perry!

I actually realized that my issue was not within using the @observer tag on it, but with the way that I was returning my component to ignition.

My component can be visualized referencing its domElement, and so in order for me to see that in Perspective, I needed to add a reference to the domElement to the mount, and then return that in my render() function.

Because of this, everytime I change a property I end up re-returning it it to the mount, which can sometimes be a bit visually taxing if I do alot of property changes (i.e. Changing the fill value of a tank).

So I was trying to optimize a bit, and make changes to the domElement without having to run the render() function, like an “On change event” for the properties. But after quite a bit of digging, I think I understand now that the “On change event” I am looking for IS the render() function. So I think it was just my misunderstanding, unless you can tell me there is another way and I am just missing it.

This is a very stripped down version of how I am doing it now, with a note as to where I currently can detect property changes.

import * as React from 'react';
import { observer } from 'mobx-react';
import { Component, ComponentMeta, ComponentProps, SizeObject } from '@inductiveautomation/perspective-client';

import * as THREE from '../include/src/three';

export const COMPONENT_TYPE = "ThreeD.display.Viewer";

export interface ViewerProps {
    objects: Array<any>;
}

@observer
export class Viewer extends Component<ComponentProps, any> {
    mount: any;
    renderer: any;
    
  constructor(props: any) {
    super(props);
  }

    componentDidMount() {
        // This is ultimately the item that I want to see in Perspective
        this.renderer = new THREE.WebGLRenderer({ antialias: true , alpha: true});

        // This is how I am adding a reference to that item to the mount
        this.mount.appendChild(this.renderer.domElement);

      }

    animate(componentProps) {
      // This is where the function that reanimates the object is, based off the property I am watching

    }

    render() {
        // This is where I can reference any property changes
        const componentProps = this.props.props.read('PropertyName');

        // This is where I can am reanimating my component based off the new properties
        this.animate(componentProps); 

        //This is where I am supplying the renderer to the mount, so that I can see it in perspective
        return (
            <div
                {...this.props.emit()}
                ref={(mount: any) => { this.mount = mount; }}
            />
        );
      }
}


// this is the actual thing that gets registered with the component registry
export class ViewerMeta implements ComponentMeta {

    getComponentType(): string {
        return COMPONENT_TYPE;
    }

    // the class or React Type that this component provides
    getViewClass(): React.ReactType {
        return Viewer;
    }

    getDefaultSize(): SizeObject {
        return ({
            width: 360,
            height: 360
        });
    }
}

I am very new to JS, React, and mob-x so please bear with me if I am misunderstanding something obvious.

I greatly appreciate your help!

Hi Keith,

You’re diving into the deep end of the ‘web development’ pool with React, Refs, Mobx, etc. So, don’t feel like you’re going a little crazy if this seems more complex than it should be. In many ways, the front-end web world is crazy, and trying to play in it the way we are definitely adds a layer or two of complexity. Perspective development is challenging even for seasoned web developers that know all the tools already, so definitely be kind and patient with yourself (and us ;)) while starting to learn this set of technologies.

It does sound like you’ve caught a bit of the gist as to what is going on: React is a ‘reactive’ UI paradigm in the sense that it attempts to only draw things to the screen when it needs to. It does this be building up a full ‘shadow dom’ structure that it manages, ultimately mirroring its’ computed structure to the ‘real’ dom.

“When it needs to” is generally when there are changes to ‘special’ values that React is watching for: React Props, React State, Context, etc . The structure of components returned from a render() function is also tracked and rebuilt/replaced when necessary. This process is something they call reconciliation and is pretty critical in understanding how or why something shows up and/or updates/renders in ‘plain’ React.

To really be comfortable and productive in building Perspective components, one really needs to be comfortable with vanilla React at a fairly deep level. Teaching React is a little out of scope for us, and even if we wanted to, we’d probably fail to improve available guides given how fast the Web world changes, but fundamentally, it’s critical to know and understand. I’d go so far as to suggest learning react outside the bounds of Perspective, to better appreciate where it leaves off, and Ignition/Perspective begin.

To that end, I would suggest reading the React Docs from cover to cover. Working knowledge of the Advanced topics in the docs will prove very helpful. In your case, the Ref section is something worth paying special attention to, as Refs can be a bit tricky, and it sounds like you may be running into forced re-rendering. I suggest going through those React docs with some intent to internalize the content, not just to get a ‘high-level view’. Having that knowledge in your pocket will help avoid some of React’s gotchas, and there are definitely a few.

For instance, taking a look at your snippet, I see:

return (
            <div
                {...this.props.emit()}
                ref={(mount: any) => { this.mount = mount; }}
            />
        );

This makes me wonder if your component/node are re-rendering unnecessarily because you’re using a lambda function as a component prop. I’d have to verify in the case of a ref, but for most ‘standard’ props, this is problematic because the lamba syntax results in a new function being created each time render is called. That scenario is a performance concern in React because (due to how Reconciliation works) these props will always get a new function reference, so even though the body of the function is identical, react just sees a new function reference was provided. The result? Component will always get re-rendered, even if all other conditions have not changed. Don’t think that’s what you want!

Instead, you might try using the Ref callback api as shown in the React docs:

@observer
export class Viewer extends Component<ComponentProps, any> {
    mount: React.Ref;
    renderer: any;
    
  constructor(props: any) {
    super(props);
    this.mount = React.createRef();
  }

  // skipped other methods...

   render() {
        // This is where I can reference any property changes
        const componentProps = this.props.props.read('PropertyName');

        // This is where I can am reanimating my component based off the new properties
        this.animate(componentProps); 

        //This is where I am supplying the renderer to the mount, so that I can see it in perspective
        return (
            <div
                {...this.props.emit()}
                ref={this.mount}
            />
        );
      }
}

I’m not familiar with the specific element/library you’re trying to work with, but think that’s probably more in the direction of what you want in order to avoid inappropriate redrawing.

In the context of Perspective, we effectively provide the ‘state/props’ to your ‘top level’ component (the one you register with the ComponentRegistry via the ComponentMeta). We do this through a few different ‘scopes’, which are simply named groups of properties that we (Ignition/Perspective) feed into your registered component.

That we use Mob-X really isn’t something that should concern you, but unfortunately it’s leaked a bit into our API. We plan on correcting this with a future update, but until then, the main things to note are:

  1. PropertyTree (the state object you get when calling this.props.props) uses mobx internally, so any PropertyTree.read() in your render function will trigger a re-render if there is a value change. Understanding what mobx reacts to can be very important.
    a. similarly, PropertyTree.write() will write a value to a prop node, and trigger a corresponding reaction/re-render if the value is being tracked by a component
  2. Perspective Component classes (the thing you register with ComponentMeta) need to be mobx-react observables, so that they can respond to ‘tracked’ values
    a. In general, any mobx value that is ‘dereferenced/read’ in an observable component’s render() method is tracked
    a. Under the hood, ‘observable’ components have what is effectively an ‘automatic’ implementation of something like a shouldComponentReRender() (not a real thing, just for educational purposes)
  3. You do not need to use mobx for any of your component’s internal State management unless you want to. The use of React State and/or React Props on all ‘children’ of your top-level component is not only possible, but recommended by us.

Lastly, you’ll want to be careful/aware of function scope binding in the Javascript world, as there are times where this may not be what you think/want it to be, so you’ll want to bind some method. This is important to remember in react component classes. We use a decorator for this @bind, but it’s just syntactic sugar that applies the correct function scoping in the same way a constructor binding does:

class SomeThing {
   instanceValue = "";

   constructor() { 
       this.someMethod = this.someMethod.bind(this); 
   }

   someMethod(anArg) {
       this.instanceValue = anArg;   // without binding, there is no guarantee 'this' is my SomeClass object!    
   }
}

Hopefully this helps. Feel free to create a new post if you have specific questions.

-Perry

6 Likes

So yes the new SDK fixed our issues. It maybe interresting to note that we had to rewrap our components into the new SDK as just adding the @observable from mobx-react in the old sdk did not fix the issue. It’s still unclear to us what caused the issue in the old SDK example but we have stopped trying to figure it out and simply migrated to the new one.

Thanks @PerryAJ for the help.

2 Likes