# Lightning Web Components Best Practices This guide provides comprehensive best practices for building production-ready LWC components, organized around the **PICKLES Framework** and incorporating advanced patterns from industry experts. --- ## PICKLES Framework Overview The PICKLES Framework provides a structured approach to LWC architecture. Use it as a checklist during component design and implementation. ``` 🥒 P - Prototype → Validate ideas with wireframes & mock data 🥒 I - Integrate → Choose data source (LDS, Apex, GraphQL) 🥒 C - Composition → Structure component hierarchy & communication 🥒 K - Kinetics → Handle user interactions & event flow 🥒 L - Libraries → Leverage platform APIs & base components 🥒 E - Execution → Optimize performance & lifecycle hooks 🥒 S - Security → Enforce permissions & data protection ``` **Reference**: [PICKLES Framework](https://www.salesforceben.com/the-ideal-framework-for-architecting-salesforce-lightning-web-components/) — David Picksley, Third Eye Consulting --- ## Component Design Principles ### Single Responsibility (PICKLES: Composition) Each component should do one thing well. ``` ✅ GOOD: accountCard, accountList, accountForm (separate components) ❌ BAD: accountManager (does display, list, and form in one) ``` ### Composition Over Inheritance Build complex UIs by composing simple components. ```html ``` ### Unidirectional Data Flow Data flows down (props), events bubble up. ``` ┌─────────────────────────────────────────────────────────────────┐ │ DATA FLOW PATTERN │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ Parent Component │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ state: accounts = [...] │ │ │ │ │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ │ │ Child A │ ←── │ Child B │ ←── │ Child C │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ @api │ │ @api │ │ @api │ │ │ │ │ │ accounts │ │ selected │ │ details │ │ │ │ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │ │ │ │ │ │ │ │ │ │ │ Events │ Events │ Events │ │ │ │ └────────────────┴────────────────┘ │ │ │ │ ↑ bubbles to parent │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────┘ ``` --- ## Naming & Decorator Conventions ### Property & Attribute Naming | Context | Convention | Example | |---------|-----------|---------| | JavaScript properties | camelCase | `itemName`, `maxValue` | | HTML attributes | kebab-case, lowercase | `item-name`, `max-value` | | Dispatched event names | Lowercase, no `on` prefix | `'recordchange'`, `'save'` | | HTML event listeners | `on` + event name | `onrecordchange`, `onsave` | Reserved prefixes in JS property names: `on`, `aria`, `data`. Reserved words: `slot`, `part`, `is`. ### @api Decorator Rules - Only one decorator per field/method — do not combine `@api` with `@track` or `@wire` - For getter/setter pairs: decorate only the getter, and always define both getter and setter - Never mutate `@api` properties internally — use a private reactive copy instead - Only use `@api` on properties/methods that are part of the component's public API ```javascript // ✅ GOOD: getter/setter with @api on getter only _recordId; @api get recordId() { return this._recordId; } set recordId(value) { this._recordId = value; this.loadRecord(); } ``` ### @track Decorator Rules Since Spring '20, primitive properties are reactive by default. `@track` is only needed when **mutating nested properties** of objects or arrays. | Scenario | @track Needed? | |----------|----------------| | Primitive value (`string`, `number`, `boolean`) | No | | Object/array that is **reassigned** entirely | No | | Object with **nested property mutation** (`this.obj.nested.value++`) | Yes | ```javascript // ❌ Unnecessary: primitives are reactive by default @track searchTerm = ''; // ✅ Correct: remove @track for primitives searchTerm = ''; // ✅ Correct: @track needed for nested mutation @track formData = { billing: { city: '' } }; // later: this.formData.billing.city = 'San Francisco'; ``` --- ## Data Integration (PICKLES: Integrate) ### Data Source Decision Tree | Scenario | Recommended Approach | |----------|---------------------| | Single record by ID | Lightning Data Service (`getRecord`) | | Simple record CRUD | `lightning-record-form` / `lightning-record-edit-form` | | Complex queries | Apex with `@AuraEnabled(cacheable=true)` | | Related records, filtering | GraphQL wire adapter | | Real-time updates | Platform Events / Streaming API | | External data | Named Credentials + Apex callout | ### GraphQL vs Apex Decision | Use GraphQL When | Use Apex When | |------------------|---------------| | Fetching related objects | Complex business logic | | Client-side filtering | Aggregate queries (COUNT, SUM) | | Cursor-based pagination | Bulk DML operations | | Reducing over-fetching | Callouts to external systems | ### Wire Service Best Practices ```javascript // Store wire result for refreshApex wiredAccountsResult; @wire(getAccounts, { searchTerm: '$searchTerm' }) wiredAccounts(result) { this.wiredAccountsResult = result; // Store for refresh const { data, error } = result; if (data) { this.accounts = data; this.error = undefined; } else if (error) { this.error = this.reduceErrors(error); this.accounts = []; } } // Refresh when needed async handleRefresh() { await refreshApex(this.wiredAccountsResult); } ``` ### Error Handling Pattern ```javascript // Centralized error reducer reduceErrors(errors) { if (!Array.isArray(errors)) { errors = [errors]; } return errors .filter(error => !!error) .map(error => { // UI API errors if (error.body?.message) return error.body.message; // JS errors if (error.message) return error.message; // GraphQL errors if (error.graphQLErrors) { return error.graphQLErrors.map(e => e.message).join(', '); } return JSON.stringify(error); }) .join('; '); } ``` --- ## Event Patterns (PICKLES: Kinetics) ### Custom Events ```javascript // Child dispatches event this.dispatchEvent(new CustomEvent('select', { detail: { recordId: this.recordId }, bubbles: true, // Bubbles through DOM composed: true // Crosses shadow boundary })); // Parent handles event handleSelect(event) { const recordId = event.detail.recordId; } ``` ### Event Bubbling Configuration Choose the minimum propagation scope needed: | Configuration | Encapsulation | Use Case | |---------------|---------------|----------| | `{ bubbles: false, composed: false }` | **Maximum** (Preferred) | Direct parent-child communication | | `{ bubbles: true, composed: false }` | Acceptable | Internal shadow DOM communication | | `{ bubbles: false, composed: true }` | Acceptable | Cross shadow boundary without full bubbling | | `{ bubbles: true, composed: true }` | **Discouraged** | Only when grandparent+ must handle event | ```javascript // ✅ Preferred: Maximum encapsulation this.dispatchEvent(new CustomEvent('select', { detail: { recordId: this.recordId } // bubbles and composed default to false })); // ⚠️ Use only when necessary this.dispatchEvent(new CustomEvent('globalnotify', { detail: { message: 'Record saved' }, bubbles: true, composed: true })); ``` ### Event Data Passing ```javascript // Primitives: pass directly in detail this.dispatchEvent(new CustomEvent('update', { detail: this.recordId // string — no wrapping needed })); // Non-primitives: always pass a copy to prevent mutation this.dispatchEvent(new CustomEvent('change', { detail: { ...this.formData } // shallow copy })); ``` ### Event Naming Conventions ``` ✅ GOOD ❌ BAD ──────────────────────── ──────────────────────── onselect onSelectItem onrecordchange on-record-change onsave onSaveClicked onerror onErrorOccurred ``` ### When to Use LMS vs Events | Scenario | Use | |----------|-----| | Parent-child communication | Custom events | | Sibling components (same parent) | Events via parent | | Components on different parts of page | Lightning Message Service | | LWC to Aura communication | LMS | | LWC to Visualforce | LMS | ### Debouncing Pattern ```javascript delayTimeout; handleSearch(event) { const searchTerm = event.target.value; clearTimeout(this.delayTimeout); this.delayTimeout = setTimeout(() => { this.searchTerm = searchTerm; }, 300); // 300ms debounce } ``` --- ## Spread Patterns & Destructuring ### lwc:spread Directive The `lwc:spread` directive dynamically spreads object properties as component attributes. Useful for reducing boilerplate and enabling dynamic attribute binding. **Reference**: [Saurabh Samir - lwc:spread Directive](https://medium.com/@saurabh.samirs) #### Basic Usage ```html ``` ```javascript get buttonAttributes() { return { label: this.buttonLabel, variant: this.isImportant ? 'brand' : 'neutral', disabled: this.isProcessing }; } ``` #### lwc:spread vs @api Object Binding | Approach | Use When | Reactivity | |----------|----------|------------| | `lwc:spread={obj}` | Passing multiple attributes dynamically | Re-renders on object change | | `@api config` | Passing structured data to custom component | Must spread in child | | Individual `@api` props | Simple, known properties | Each prop triggers render | #### Conditional Attribute Spreading ```javascript get inputAttributes() { const attrs = { label: 'Search', type: 'text', value: this.searchTerm }; // Conditionally add attributes if (this.isRequired) { attrs.required = true; } if (this.maxLength) { attrs['max-length'] = this.maxLength; } return attrs; } ``` ```html ``` #### Event Handlers with lwc:spread **Important**: Event handlers must be bound separately, not spread: ```html ``` ### lwc:on Directive (Spring '26 - API 66.0) The `lwc:on` directive solves the limitation above by enabling **dynamic event binding** directly from JavaScript. It allows you to bind multiple event handlers at runtime. **Requires**: API 66.0+ (Spring '26) #### Basic Usage ```javascript // component.js export default class DynamicEventComponent extends LightningElement { // Define event handlers as object properties eventHandlers = { click: this.handleClick.bind(this), mouseover: this.handleMouseOver.bind(this), focus: this.handleFocus.bind(this) }; handleClick() { console.log('Element clicked!'); } handleMouseOver() { console.log('Mouse over!'); } handleFocus() { console.log('Element focused!'); } } ``` ```html ``` #### Combining lwc:spread and lwc:on For fully dynamic components, combine both directives: ```javascript // component.js export default class FullyDynamicButton extends LightningElement { // Properties via lwc:spread buttonAttributes = { label: 'Save', variant: 'brand', disabled: false }; // Events via lwc:on buttonEvents = { click: this.handleClick.bind(this), focus: this.handleFocus.bind(this) }; handleClick() { this.dispatchEvent(new CustomEvent('save')); } handleFocus() { console.log('Button focused'); } } ``` ```html ``` #### Dynamic Event Handlers from @api Pass event handler configurations from parent components: ```javascript // childComponent.js export default class ChildComponent extends LightningElement { @api eventConfig; // { click: handler, change: handler } get resolvedHandlers() { // Ensure handlers are properly bound const handlers = {}; if (this.eventConfig) { Object.entries(this.eventConfig).forEach(([event, handler]) => { handlers[event] = typeof handler === 'function' ? handler : () => {}; }); } return handlers; } } ``` ```html ``` #### Removing Event Listeners Remove specific event listeners by omitting them from the object: ```javascript // Toggle mouseover handler on/off toggleHoverHandler() { if (this._hoverEnabled) { // Remove mouseover by omitting it this.eventHandlers = { click: this.handleClick.bind(this) }; } else { // Add mouseover back this.eventHandlers = { click: this.handleClick.bind(this), mouseover: this.handleMouseOver.bind(this) }; } this._hoverEnabled = !this._hoverEnabled; } ``` #### lwc:spread vs lwc:on Comparison | Directive | Purpose | Use Case | |-----------|---------|----------| | `lwc:spread` | Dynamic **properties/attributes** | Pass label, variant, disabled dynamically | | `lwc:on` | Dynamic **event handlers** | Bind click, change, custom events dynamically | | Both together | Fully dynamic configuration | Reusable wrapper components, dynamic UIs | **Important Notes**: - Do NOT mutate the object passed to `lwc:on` - create a new object to update handlers - Event type names should be lowercase without the `on` prefix (use `click` not `onclick`) - Always use `.bind(this)` or arrow functions to preserve context ### Object Spread & Destructuring Modern JavaScript patterns for cleaner data handling in LWC. #### Object Spread for Config Merging ```javascript // Default + user config pattern const defaultConfig = { pageSize: 10, sortField: 'Name', sortDirection: 'ASC' }; get tableConfig() { return { ...defaultConfig, ...this.userConfig // User config overrides defaults }; } ``` #### Destructuring with Defaults ```javascript // Extract values with fallbacks handleRecordLoad(record) { const { Name = 'Unknown', Industry = 'Not Specified', AnnualRevenue = 0 } = record.fields; this.accountName = Name.value; this.industry = Industry.value; this.revenue = AnnualRevenue.value; } ``` #### Nested Destructuring ```javascript // Deep extraction in single statement processResult(result) { const { data: { record: { fields: { Name, BillingCity } } }, error } = result; if (error) { this.handleError(error); return; } this.name = Name.value; this.city = BillingCity.value; } ``` #### Array Spread Patterns ```javascript // Immutable array updates (required for LWC reactivity) addItem(newItem) { this.items = [...this.items, newItem]; // Append } removeItem(index) { this.items = [ ...this.items.slice(0, index), ...this.items.slice(index + 1) ]; // Remove at index } updateItem(index, updates) { this.items = this.items.map((item, i) => i === index ? { ...item, ...updates } : item ); // Update at index } ``` #### Parameter Spreading in Apex Calls ```javascript async handleSubmit() { const result = await createRecord({ ...this.recordData, CreatedBy__c: this.currentUserId, Status__c: 'Pending' }); } ``` ### When to Use Each Pattern | Pattern | Best For | Avoid When | |---------|----------|------------| | `lwc:spread` | Many dynamic attributes, base component wrappers | Need event binding, simple static props | | Object spread | Config merging, immutable updates | Deep objects (consider structuredClone) | | Destructuring | Extracting multiple values, API responses | Simple single-property access | | Array spread | Adding/removing items immutably | Large arrays (performance concern) | --- ## Complex Template Expressions (Spring '26 Beta - API 66.0) Spring '26 introduces **complex template expressions**, enabling JavaScript expressions directly in templates. This was previously limited to simple property and getter bindings. > ⚠️ **Beta Feature**: Use getters in production until this becomes GA. Document any complex expressions for future migration. ### Before vs After ```html ``` ```javascript // Required getter in JS get showLoadingState() { return this.isLoading && this.items.length === 0; } ``` ```html ``` ### Supported Expression Types | Expression Type | Example | Notes | |-----------------|---------|-------| | **Logical NOT** | `{!isLoading}` | Negation | | **Logical AND** | `{a && b}` | Short-circuit evaluation | | **Logical OR** | `{a \|\| b}` | Short-circuit evaluation | | **Comparison** | `{count > 0}`, `{status === 'active'}` | `==`, `===`, `!=`, `!==`, `<`, `>`, `<=`, `>=` | | **Arithmetic** | `{price * quantity}` | `+`, `-`, `*`, `/`, `%` | | **Optional Chaining** | `{user?.profile?.name}` | Safe property access | | **Nullish Coalescing** | `{value ?? 'default'}` | Default for null/undefined | | **Ternary** | `{isActive ? 'Yes' : 'No'}` | Conditional value | | **Array Access** | `{items[0]}` | Index-based access | | **String Concatenation** | `{firstName + ' ' + lastName}` | String joining | ### Best Practices for Complex Expressions ```html {account?.Owner?.Name} ``` ### Migration Strategy 1. **New code**: Use complex expressions for simple conditions 2. **Existing code**: Keep getters that have unit tests 3. **Complex logic**: Continue using getters for maintainability 4. **Document**: Mark complex expressions in templates for review when GA ### Limitations (Beta) - No function calls in expressions (use getters) - No template literals with `${}` interpolation - Cannot reference `this` directly - No destructuring in expressions --- ## Template Directives ### Conditional Rendering: Legacy → Modern Always use `lwc:if`, `lwc:elseif`, `lwc:else` instead of the deprecated `if:true` / `if:false` directives. ```html ``` **Rules**: Conditional directives are valid on `