Premium support for our pure JavaScript UI components


Post by Josh Argent »

Hi,

We've come across a scenario that is causing problems for us and I'd like your advise on how you would expect this situation to be handled.

We have a number of async processes (such as loading data) that interact with the grid and store instances once the promise resolves. However, if the grid or store has been destroyed while that promise is waiting, once it resolves we get errors when it tries to interact with the grids properties or methods. As far as I can tell, when something is destroyed it removes all properties from that instance, which is why we see "Cannot read properties of null" errors.

What do you suggest we do in these scenarios? Should we be checking that the grid is not destroyed every time a something resolves?

I have put together an example using the Tree Grid demo. There are two buttons in the toolbar "Async Thing" and "Destroy". If you press "Async Thing" it triggers a setTimeout for 5 seconds which will eventually call a method on the grid. If you press "Destroy" before the timeout resolves, you end up getting an error in the console.

import { TreeGrid, GridRowModel } from '../../build/grid.module.js?463787';
import shared from '../_shared/shared.module.js?463787';

class Gate extends GridRowModel {
    static get fields() {
        return [{
            name : 'capacity',
            type : 'number'
        }, 'domestic', 'airline', 'manager'];
    }
}

// Transform a parent node to a leaf node when all its children are removed
Gate.convertEmptyParentToLeaf = true;

const tree = new TreeGrid({

appendTo : 'container',

features : {
    cellEdit   : true,
    filter     : true,
    rowReorder : true,
    stripe     : true
},

loadMask : 'Loading tree data...',

columns : [
    { text : 'Id', field : 'id', width : 40, editor : false },
    { text : 'ParentIndex', field : 'parentIndex', width : 40, hidden : true },
    {
        text        : 'Name',
        field       : 'name',
        flex        : 3,
        type        : 'tree',
        touchConfig : { editor : false }
        // You can customize expand/collapse icons
        // expandIconCls   : 'b-fa b-fa-plus-square',
        // collapseIconCls : 'b-fa b-fa-minus-square'
    },
    { type : 'aggregate', text : 'Capacity', field : 'capacity', flex : 1 },
    { text : 'Domestic', field : 'domestic', flex : 1 },
    { text : 'Airline', field : 'airline', flex : 1 },
    { text : 'Responsible<br/>Manager', field : 'manager', width : 100, htmlEncodeHeaderText : false }
],

store : {
    modelClass : Gate,
    readUrl    : 'data/kastrup-airport.json',
    autoLoad   : true
},

handleAsyncActionComplete: () => {
    console.log('async action complete!');
},

tbar : [
    {
        type        : 'button',
        ref         : 'customButton',
        icon        : 'b-fa-folder-open',
        pressedIcon : 'b-fa-plane',
        text        : 'Use custom tree icons',
        toggleable  : true,
        onToggle({ pressed }) {
            tree.store.readUrl = 'data/' + (pressed ? 'ohare-airport.json' : 'kastrup-airport.json');
            tree.element.classList[pressed ? 'add' : 'remove']('ohare-airport');
            tree.store.load();
        }
    },
    {
        type     : 'button',
        ref      : 'expandAllButton',
        icon     : 'b-fa b-fa-angle-double-down',
        text     : 'Expand all',
        onAction : () => tree.expandAll()
    },
    {
        type     : 'button',
        ref      : 'collapseAllButton',
        icon     : 'b-fa b-fa-angle-double-up',
        text     : 'Collapse all',
        onAction : () => tree.collapseAll()
    },
    {
        type     : 'button',
        text     : 'Async Thing',
        onAction : () => setTimeout(() => grid.handleAsyncActionComplete(), 5000)
    },
    {
        type     : 'button',
        text     : 'Destroy',
        onAction : () => tree.destroy()
    }
]
});

I would appreciate any suggestions you have for how to handle these situations :)

Thanks.


Post by Animal »

Use grid.setTimeout

Grid is a Widget and all Widgets are Delayables, and its own timers are all cancelled when it is destroyed.


Post by Josh Argent »

That's handy, I didn't know about grid.setTimeout!

What if it's a promise that we're waiting to resolve? Something like a call to an API to load data? We see the same problem with that.


Post by Animal »

It's just timers that are cancelled, but if the Promise is resolved by the firing of a timer, then if that timer is cancelled, the promise won't be resolved.


Post by Josh Argent »

So if the promise isn't timer based (let's say its a fetch call to an API somewhere), we have to check that the grid hasn't been destroyed before calling anything on it?


Post by Animal »

Yes, if you embark on a long (In computer terms) operation like a network request , you need to be ready to deal with conditions that apply at the time the operation finishes.

I'm not sure how components get destroyed during a running application. They should all persist until the page is exited, but if you are destroying components, then you have to code for the consequences of that.


Post by Josh Argent »

I guess you wouldn't normally expect a component to be destroyed while it's running. To add some context, this is happening inside Salesforce Lightning App Builder, whenever the LWC is moved around the page it destroys and recreates the grid.

One workaround that seems to work is to add this to all of our affected components:

doDestroy() {
    this.removeAllListeners();
}

Maybe removing all event listeners when it is destroyed should be part of the events mixin?


Post by Animal »

All event listeners are removed upon destroy. See the doDestroy method in the Events mixin.

Screenshot 2022-12-02 at 11.01.22.png
Screenshot 2022-12-02 at 11.01.22.png (421.69 KiB) Viewed 560 times

Post Reply