Tuesday, July 29, 2025

Race Conditions in Salesforce LWC Wire Adapters: A Common Pitfall

When building Lightning Web Components, you'll often need data from multiple sources. Here's a typical scenario that creates a race condition:

export default class ProductCatalog extends LightningElement {
    userPreferences = {};
    products = [];

    @wire(getUserPreferences)
    loadPreferences({ data }) {
        if (data) {
            this.userPreferences = data;
        }
    }

    @wire(getProducts, { categoryId: '$categoryId' })
    loadProducts({ data }) {
        if (data) {
            // Bug: userPreferences might not be loaded yet!
            this.products = data.filter(product => 
                product.price <= this.userPreferences.maxPrice
            );
        }
    }
}

The loadProducts wire adapter doesn't wait for loadPreferences to complete. If products load first, userPreferences.maxPrice is undefined, causing incorrect filtering.

Why It Happens

Wire adapters are:

  • Asynchronous: Execute in unpredictable order
  • Independent: Don't coordinate with each other
  • One-time reactive: Only re-run when their specific parameters change

The Solution

Here's a pattern to coordinate multiple wire adapters:

export default class ProductCatalog extends LightningElement {
    // Use undefined to track loading state
    userPreferences = undefined;
    rawProducts = undefined;
    
    get products() {
        // Only compute when both are loaded
        if (!this.rawProducts || this.userPreferences === undefined) {
            return [];
        }
        
        return this.rawProducts.filter(product => 
            product.price <= this.userPreferences.maxPrice
        );
    }

    @wire(getUserPreferences)
    loadPreferences({ data, error }) {
        if (data) {
            this.userPreferences = data;
        } else if (error) {
            this.userPreferences = null; // Explicitly set on error
        }
    }

    @wire(getProducts, { categoryId: '$categoryId' })
    loadProducts({ data, error }) {
        if (data) {
            this.rawProducts = data;
        } else if (error) {
            this.rawProducts = null;
        }
    }
}

Key Principles

  1. Use undefined for unloaded state - Distinguish between "not loaded" and "empty/false"
  2. Store raw data separately - Don't process data in wire handlers
  3. Use getters for computed values - They automatically update when dependencies change
  4. Handle errors explicitly - Set values to null on error to avoid infinite loading states

Alternative: Imperative Approach

For complex dependencies, consider imperative calls:

async connectedCallback() {
    try {
        const [preferences, products] = await Promise.all([
            getUserPreferences(),
            getProducts({ categoryId: this.categoryId })
        ]);
        
        this.processData(preferences, products);
    } catch (error) {
        this.handleError(error);
    }
}

Conclusion

Race conditions in wire adapters are subtle but solvable. By treating data loading as a coordination problem rather than independent events, you can build reliable components that work consistently regardless of network conditions.

Remember: if your component depends on multiple data sources, always implement a coordination strategy. Your users will experience a more reliable application, and you'll spend less time debugging intermittent issues.

Share This:    Facebook Twitter

0 comments:

Post a Comment

Total Pageviews

My Social Profiles

View Sonal's profile on LinkedIn

Tags

__proto__ $Browser Access Grants Accessor properties Admin Ajax AllowsCallouts Apex Apex Map Apex Sharing AssignmentRuleHeader AsyncApexJob Asynchronous Auth Provider AWS Callbacks Connected app constructor Cookie CPU Time CSP Trusted Sites CSS Custom settings CustomLabels Data properties Database.Batchable Database.BatchableContext Database.query Describe Result Destructuring Dynamic Apex Dynamic SOQL Einstein Analytics enqueueJob Enterprise Territory Management Enumeration escapeSingleQuotes featured Flows geolocation getGlobalDescribe getOrgDefaults() getPicklistValues getRecordTypeId() getRecordTypeInfosByName() getURLParameters Google Maps Governor Limits hasOwnProperty() Heap Heap Size IIFE Immediately Invoked Function Expression Interview questions isCustom() Javascript Javascript Array jsForce Lightning Lightning Components Lightning Events lightning-record-edit-form lightning:combobox lightning:icon lightning:input lightning:select LockerService Lookup LWC Manual Sharing Map Modal Module Pattern Named Credentials NodeJS OAuth Object.freeze() Object.keys() Object.preventExtensions() Object.seal() Organization Wide Defaults Override PDF Reader Performance performance.now() Permission Sets Picklist Platform events Popup Postman Primitive Types Profiles Promise propertyIsEnumerable() prototype Query Selectivity Queueable Record types Reference Types Regex Regular Expressions Relationships Rest API Rest Operator Revealing Module Pattern Role Hierarchy Salesforce Salesforce Security Schema.DescribeFieldResult Schema.DescribeSObjectResult Schema.PicklistEntry Schema.SObjectField Schema.SObjectType Security Service Components Shadow DOM Sharing Sharing Rules Singleton Slots SOAP API SOAP Web Services SOQL SOQL injection Spread Operator Star Rating stripInaccessible svg svgIcon Synchronous this Token Triggers uiObjectInfoApi Upload Files VSCode Web Services XHR
Scroll To Top