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
- Use
undefined
for unloaded state - Distinguish between "not loaded" and "empty/false" - Store raw data separately - Don't process data in wire handlers
- Use getters for computed values - They automatically update when dependencies change
- 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.
0 comments:
Post a Comment