1

I've recently noticed that if I set an aura:attribute of type "List" from a Javascript array, then later retrieve that same attribute value via component.get("v.myListAttribute"), the return value is a Proxy, not the simple array I set it to.

I'm thinking this may be new as of Winter '20, since I recently had code that was dealing with arrays retrieved via component.get() suddenly stop working. Unforunately I can't confirm this, since Summer '19 is no longer available in any environment.

To reproduce, all you need is a component with an aura:attribute like so:

<aura:attribute name="options" type="List" />

The Javascript controller reveals the interesting behavior:

({
    doInit : function(component, event, helper) {

        let newOptions = [
            {label: 'Apples', value: 'Apples'},
            {label: 'Bananas', value: 'Bananas'},
            {label: 'Cherries', value: 'Cherries'}
        ];

        component.set("v.options", newOptions);

        console.log(component.get("v.options")); // returns a Proxy
        console.log(newOptions); // returns an array
        console.log(component.get("v.options") == newOptions); // returns false
        console.log(JSON.stringify(component.get("v.options")) 
                    == JSON.stringify(newOptions)); // returns true

    }
})

Can anyone confirm if this is new behavior? What is the best way to work with an array that has been retrieved from an aura:attribute? Is there a better way, outside of JSON.stringify() / JSON.parse() ...?

A more real-world-ish example, per request

Requirements: If the user has already selected a product option and subsequently changes the effective date so that the product options will change, prompt them to confirm their update. If the effective date changes in a way that will not impact the product options, do not prompt for a confirmation.

The component HTML

<aura:component implements="force:appHostable,flexipage:availableForAllPageTypes,lightning:actionOverride" access="global">

    <aura:attribute name="effectiveDate" type="Date" default="2019-12-01" />
    <aura:attribute name="productOptions" type="List" />
    <aura:attribute name="productSelection" type="String" />
    <aura:attribute name="suspendConfirm" type="Boolean" />

    <aura:handler name="init" value="{!this}" action="{!c.doInit}"/>
    <aura:handler name="change" value="{!v.effectiveDate}" action="{!c.onDateChange}" />

    <lightning:input type="date" label="Effective Date" value="{!v.effectiveDate}" />
    <lightning:radioGroup label="Product" options="{!v.productOptions}" 
                          value="{!v.productSelection}" />

</aura:component>

The Javascript controller

({
    doInit : function(component, event, helper) {
        helper.setProductOptions(component, event);
    },

    onDateChange : function(component, event, helper) {
        helper.setProductOptions(component, event);
    }
})

The Javascript helper

({
    setProductOptions : function(component, event) {

        let productOptions2019 = [
            {label: 'Apples', value: 'P0001'},
            {label: 'Bananas', value: 'P0002'},
            {label: 'Cherries', value: 'P0003'}
        ];

        let productOptions2020 = [
            {label: 'Apples', value: 'X0011'},
            {label: 'Pears', value: 'X0012'},
            {label: 'Figs', value: 'X0013'}
        ];

        let newProductOptions;
        let effDate = new Date(component.get('v.effectiveDate'));
        if (effDate >= new Date('2020-01-01')) {
            newProductOptions = productOptions2020;
        } else {
            newProductOptions = productOptions2019;
        }

        // This is where I want to compare a new Javascript array with an existing 
        // aura:attribute of type List
        if (component.get('v.productSelection') && 
            newProductOptions !== component.get('v.productOptions') && 
            !component.get('v.suspendConfirm')) {
            if(confirm('You changed the year.  Your product selection will be cleared.')) {
                component.set('v.productOptions', newProductOptions);
            } else {
                component.set('v.suspendConfirm', true);
                component.set('v.effectiveDate', event.getParam('oldValue'));
                component.set('v.suspendConfirm', false);
            }
        } else {
            component.set('v.productOptions', newProductOptions);
        }

    }
})

The Solution

I now have a much better understanding of what it takes to evaluate array and object equality in Javascript! Referenced this thread, and replaced helper code as follows. Note that since we are dealing with not just an array but an array of objects, the helper function isArrayOfObjectsEqual(array1, array2) is a bit more involved than what salesforce-sas put forward.

It's worth noting that in my case, the fact that the array is returned from the aura:attribute as a proxy was a red herring. Even if it hadn't been a proxy, I wouldn't have been able to use equality operators in the way I was wanting to.

({
    setProductOptions : function(component, event) {

        let productOptions2019 = [
            {label: 'Apples', value: 'P0001'},
            {label: 'Bananas', value: 'P0002'},
            {label: 'Cherries', value: 'P0003'}
        ];

        let productOptions2020 = [
            {label: 'Apples', value: 'X0011'},
            {label: 'Pears', value: 'X0012'},
            {label: 'Figs', value: 'X0013'}
        ];

        let newProductOptions;
        let effDate = new Date(component.get('v.effectiveDate'));
        if (effDate >= new Date('2020-01-01')) {
            newProductOptions = productOptions2020;
        } else {
            newProductOptions = productOptions2019;
        }

        let productOptions = component.get('v.productOptions');

        if (component.get('v.productSelection') && !this.isArrayOfObjectsEqual(newProductOptions,component.get('v.productOptions')) && !component.get('v.suspendConfirm')) {
            if(confirm('You changed the year.  Your product selection will be cleared.')) {
                component.set('v.productOptions', newProductOptions);
            } else {
                component.set('v.suspendConfirm', true);
                component.set('v.effectiveDate', event.getParam('oldValue'));
                component.set('v.suspendConfirm', false);
            }
        } else {
            component.set('v.productOptions', newProductOptions);
        }

    },

    isArrayOfObjectsEqual : function(array1, array2) {
        if (array1.length === array2.length) {
            return array1.every(function(object1, arrIndex) { 
                let object2 = array2[arrIndex];
                let object1Props = Object.getOwnPropertyNames(object1);
                let object2Props = Object.getOwnPropertyNames(object2);
                if (object1Props.length !== object2Props.length) {
                    return false;
                }
                return object1Props.every(function(prop1, propIndex) {
                    let prop2 = object2Props[propIndex];
                    return (prop1 === prop2 && object1[prop1] === object2[prop2]);
                });
            });
        }
        return false;
    }

})
  • 1
    Can you provide an example of code on how you are trying to use the list in a real world example? I don't expect == to ever function correctly but that should be mostly irrelevant. The proxy should be functionally identical to an array. – gNerb Oct 17 '19 at 23:09
  • 1
    This has been standard for a while now - anytime you store your array in the attribute, it gets turned into a proxy. Of course, you could just serialize everything via your own storage scheme and content provider if you wanted to avoid this. – Caspar Harmer Oct 18 '19 at 1:17
  • What is the problem you are facing? you have given "A more real-world-ish example, per request" but did not tell what is the prob you are facing? – salesforce-sas Oct 18 '19 at 5:13
2

Lightning Aura attributes returns proxy objects from components of API > 40. This is because Lightning Locker Uses the Proxy Object.

However, it does not affect the comparison because

You can interact with a Proxy object in the same way as you interact with the raw JavaScript object, but the object shows up in the browser’s console as a Proxy.

There are 2 problems with your code:

Problem 1:

Arrays cannot be compared directly. And so newProductOptions !== component.get('v.productOptions') will always return true`. You can check that in browser console PFB:

enter image description here

You can compare arrays using a sample method isArrayEquals like below:

console.log(isArrayEquals(['1'],[1])); // false
console.log(isArrayEquals(['1'],['1'])); // true

function isArrayEquals(arr1, arr2) {
  return (arr2.length !== arr1.length || arr2.filter((item,ind) => arr1[ind]!==item).length===0);
}

// this.isArrayEquals(newProductOptions, component.get('v.productOptions'))

Problem 2:

You are not passing the event parameter to helper and thats the reason your code is not working. Change the controller.js to below:

CONTROLLER:

({
    doInit : function(component, event, helper) {
        helper.setProductOptions(component, event);
    },

    onDateChange : function(component, event, helper) {
        helper.setProductOptions(component, event);
    }
})
  • Thanks for putting me on the right path! I edited my post to provide the right params in the controller (didn't want that to be a distraction from the main question). Your isArrayEquals function didn't work in this case since it we are dealing with an array of objects, and objects face similar constraints to arrays. See my edited post above, and thanks for the helpful guidance. :-) – Matthew Souther Oct 18 '19 at 17:50

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.