# LWC Component Patterns Comprehensive code examples for common Lightning Web Component patterns. --- ## Table of Contents 1. [PICKLES Framework Details](#pickles-framework-details) 2. [Wire Service Patterns](#wire-service-patterns) - [Wire vs Imperative Apex Calls](#wire-vs-imperative-apex-calls) 3. [GraphQL Patterns](#graphql-patterns) 4. [Modal Component Pattern](#modal-component-pattern) 5. [Record Picker Pattern](#record-picker-pattern) 6. [Workspace API Pattern](#workspace-api-pattern) 7. [Parent-Child Communication](#parent-child-communication) 8. [Sibling Communication (via Parent)](#sibling-communication-via-parent) 9. [Navigation Patterns](#navigation-patterns) 10. [TypeScript Patterns](#typescript-patterns) 11. [Apex Controller Patterns](#apex-controller-patterns) --- ## PICKLES Framework Details ### P - Prototype **Purpose**: Validate ideas early before full implementation. | Action | Description | |--------|-------------| | Wireframe | Create high-level component sketches | | Mock Data | Use sample data to test functionality | | Stakeholder Review | Gather feedback before development | | Separation of Concerns | Break into smaller functional pieces | ```javascript // Mock data pattern for prototyping const MOCK_ACCOUNTS = [ { Id: '001MOCK001', Name: 'Acme Corp', Industry: 'Technology' }, { Id: '001MOCK002', Name: 'Global Inc', Industry: 'Finance' } ]; export default class AccountPrototype extends LightningElement { accounts = MOCK_ACCOUNTS; // Replace with wire/Apex later } ``` ### I - Integrate **Purpose**: Determine how components interact with data systems. **Integration Checklist**: - [ ] Implement error handling with clear user notifications - [ ] Add loading spinners to prevent duplicate requests - [ ] Use LDS for single-object operations (minimizes DML) - [ ] Respect FLS and CRUD in Apex implementations - [ ] Store `wiredResult` for `refreshApex()` support ### C - Composition **Purpose**: Structure how LWCs nest and communicate. **Best Practices**: - Maintain shallow component hierarchies (max 3-4 levels) - Single responsibility per component - Clean up subscriptions in `disconnectedCallback()` - Use custom events purposefully, not for every interaction ```javascript // Parent-managed composition pattern // parent.js handleChildEvent(event) { this.selectedId = event.detail.id; // Update child via @api this.template.querySelector('c-child').selectedId = this.selectedId; } ``` ### K - Kinetics **Purpose**: Manage user interaction and event responsiveness. ```javascript // Debounce pattern for search delayTimeout; handleSearchChange(event) { const searchTerm = event.target.value; clearTimeout(this.delayTimeout); this.delayTimeout = setTimeout(() => { this.dispatchEvent(new CustomEvent('search', { detail: { searchTerm } })); }, 300); } ``` ### L - Libraries **Purpose**: Leverage Salesforce-provided and platform tools. **Recommended Platform Features**: | API/Module | Use Case | |------------|----------| | `lightning/navigation` | Page/record navigation | | `lightning/uiRecordApi` | LDS operations (getRecord, updateRecord) | | `lightning/platformShowToastEvent` | User notifications | | `lightning/modal` | Native modal dialogs | | Base Components | Pre-built UI (button, input, datatable) | | `lightning/refresh` | Dispatch refresh events | **Avoid reinventing** what base components already provide! ### E - Execution **Purpose**: Optimize performance and resource efficiency. **Performance Checklist**: - [ ] Lazy load with `if:true` / `lwc:if` - [ ] Use `key` directive in iterations - [ ] Cache computed values in getters - [ ] Avoid property updates that trigger re-renders - [ ] Use browser DevTools Performance tab ### S - Security **Purpose**: Enforce access control and data protection. ```apex // Secure Apex pattern @AuraEnabled(cacheable=true) public static List getAccounts(String searchTerm) { String searchKey = '%' + String.escapeSingleQuotes(searchTerm) + '%'; return [ SELECT Id, Name, Industry FROM Account WHERE Name LIKE :searchKey WITH SECURITY_ENFORCED LIMIT 50 ]; } ``` --- ## Wire Service Patterns ### Wire vs Imperative Apex Calls LWC can interact with Apex in two ways: **@wire** (reactive/declarative) and **imperative calls** (manual/programmatic). Understanding when to use each is critical for building performant, maintainable components. #### Quick Comparison ```text ┌─────────────────────────────────────────────────────────────────────────────────────┐ │ WIRE vs IMPERATIVE APEX CALLS │ ├──────────────────┬──────────────────────────────┬────────────────────────────────────┤ │ Aspect │ Wire (@wire) │ Imperative Calls │ ├──────────────────┼──────────────────────────────┼────────────────────────────────────┤ │ Execution │ Automatic / Reactive │ Manual / Programmatic │ │ DML Operations │ ❌ Read-Only │ ✅ Insert / Update / Delete │ │ Data Updates │ ✅ Auto on Parameter Change │ ❌ Manual Refresh Required │ │ Control │ ⚠️ Low (framework decides) │ ✅ Full (you decide when/how) │ │ Error Handling │ ✅ Framework Managed │ ⚠️ Developer Managed │ │ Supported Objects│ ⚠️ UI API Only │ ✅ All Objects │ │ Caching │ ✅ Built-in (cacheable=true) │ ❌ No automatic caching │ └──────────────────┴──────────────────────────────┴────────────────────────────────────┘ ``` #### Pros & Cons | Wire (@wire) | Imperative Calls | |--------------|------------------| | ✅ Auto UI sync & caching | ✅ Supports DML & all objects | | ✅ Less boilerplate code | ✅ Full control over timing | | ✅ Reactive to parameter changes | ✅ Can handle complex logic | | ❌ Read-only, limited objects | ❌ Manual handling, no auto refresh | | ❌ Can't control execution timing | ❌ More error handling code needed | #### When to Use Each **Use Wire (@wire) when:** - 📌 Read-only data display - 📌 Auto-refresh UI when parameters change - 📌 Stable parameters (recordId, filter values) - 📌 Working with UI API supported objects **Use Imperative Calls when:** - 📌 User actions (clicks, form submissions) - 📌 DML operations (Insert, Update, Delete) - 📌 Dynamic parameters determined at runtime - 📌 Custom objects or complex queries - 📌 Need control over execution timing #### Side-by-Side Code Examples **Wire Example** - Data loads automatically when `selectedIndustry` changes: ```javascript import { LightningElement, wire } from 'lwc'; import fetchAccounts from '@salesforce/apex/AccountController.fetchAccounts'; export default class WireExample extends LightningElement { selectedIndustry = 'Technology'; accounts; error; // Automatically re-fetches when selectedIndustry changes @wire(fetchAccounts, { industry: '$selectedIndustry' }) wiredAccounts({ data, error }) { if (data) { this.accounts = data; this.error = undefined; } else if (error) { this.error = error; this.accounts = undefined; } } } ``` **Imperative Example** - Data loads only when user triggers action: ```javascript import { LightningElement } from 'lwc'; import fetchAccounts from '@salesforce/apex/AccountController.fetchAccounts'; export default class ImperativeExample extends LightningElement { selectedIndustry = 'Technology'; accounts; error; isLoading = false; // Called explicitly when user clicks button or submits form async fetchAccounts() { this.isLoading = true; try { this.accounts = await fetchAccounts({ industry: this.selectedIndustry }); this.error = undefined; } catch (error) { this.error = error; this.accounts = undefined; } finally { this.isLoading = false; } } } ``` #### Decision Tree ```text ┌─────────────────────────────┐ │ Need to modify data? │ │ (Insert/Update/Delete) │ └─────────────┬───────────────┘ │ ┌─────────────┴───────────────┐ │ │ YES NO │ │ ▼ ▼ ┌─────────────────┐ ┌─────────────────────────┐ │ IMPERATIVE │ │ Should data auto- │ │ (Use await) │ │ refresh on param │ └─────────────────┘ │ change? │ └───────────┬─────────────┘ │ ┌───────────┴───────────┐ │ │ YES NO │ │ ▼ ▼ ┌─────────────────┐ ┌─────────────────┐ │ @WIRE │ │ IMPERATIVE │ │ (Reactive) │ │ (On-demand) │ └─────────────────┘ └─────────────────┘ ``` --- ### 1. Basic Data Display (Wire Service) ```javascript // accountCard.js import { LightningElement, api, wire } from 'lwc'; import { getRecord, getFieldValue } from 'lightning/uiRecordApi'; import NAME_FIELD from '@salesforce/schema/Account.Name'; import INDUSTRY_FIELD from '@salesforce/schema/Account.Industry'; const FIELDS = [NAME_FIELD, INDUSTRY_FIELD]; export default class AccountCard extends LightningElement { @api recordId; @wire(getRecord, { recordId: '$recordId', fields: FIELDS }) account; get name() { return getFieldValue(this.account.data, NAME_FIELD); } get industry() { return getFieldValue(this.account.data, INDUSTRY_FIELD); } get isLoading() { return !this.account.data && !this.account.error; } } ``` ```html ``` ### 2. Wire Service with Apex ```javascript // contactList.js import { LightningElement, api, wire } from 'lwc'; import getContacts from '@salesforce/apex/ContactController.getContacts'; import { refreshApex } from '@salesforce/apex'; export default class ContactList extends LightningElement { @api recordId; contacts; error; wiredContactsResult; @wire(getContacts, { accountId: '$recordId' }) wiredContacts(result) { this.wiredContactsResult = result; // Store for refreshApex const { error, data } = result; if (data) { this.contacts = data; this.error = undefined; } else if (error) { this.error = error; this.contacts = undefined; } } async handleRefresh() { await refreshApex(this.wiredContactsResult); } } ``` --- ## GraphQL Patterns > **Module Note**: `lightning/graphql` supersedes `lightning/uiGraphQLApi` and provides newer features like mutations, optional fields, and dynamic query construction. ### GraphQL Query (Wire Adapter) ```javascript // graphqlContacts.js import { LightningElement, wire } from 'lwc'; import { gql, graphql } from 'lightning/graphql'; const CONTACTS_QUERY = gql` query ContactsQuery($first: Int, $after: String) { uiapi { query { Contact(first: $first, after: $after) { edges { node { Id Name { value } Email { value } Account { Name { value } } } cursor } pageInfo { hasNextPage endCursor } } } } } `; export default class GraphqlContacts extends LightningElement { contacts; pageInfo; error; _cursor; @wire(graphql, { query: CONTACTS_QUERY, variables: '$queryVariables' }) wiredContacts({ data, error }) { if (data) { const result = data.uiapi.query.Contact; this.contacts = result.edges.map(edge => ({ id: edge.node.Id, name: edge.node.Name.value, email: edge.node.Email?.value, accountName: edge.node.Account?.Name?.value })); this.pageInfo = result.pageInfo; } else if (error) { this.error = error; } } get queryVariables() { return { first: 10, after: this._cursor }; } loadMore() { if (this.pageInfo?.hasNextPage) { this._cursor = this.pageInfo.endCursor; } } } ``` ### GraphQL Mutations (Spring '26 - GA in API 66.0) Mutations allow create, update, and delete operations via GraphQL. Use `executeMutation` for imperative operations. ```javascript // graphqlAccountMutation.js import { LightningElement, track } from 'lwc'; import { gql, executeMutation } from 'lightning/graphql'; import { ShowToastEvent } from 'lightning/platformShowToastEvent'; // Create mutation const CREATE_ACCOUNT = gql` mutation CreateAccount($name: String!, $industry: String) { uiapi { AccountCreate(input: { Account: { Name: $name Industry: $industry } }) { Record { Id Name { value } Industry { value } } } } } `; // Update mutation const UPDATE_ACCOUNT = gql` mutation UpdateAccount($id: ID!, $name: String!) { uiapi { AccountUpdate(input: { Account: { Id: $id Name: $name } }) { Record { Id Name { value } } } } } `; // Delete mutation const DELETE_ACCOUNT = gql` mutation DeleteAccount($id: ID!) { uiapi { AccountDelete(input: { Account: { Id: $id } }) { Id } } } `; export default class GraphqlAccountMutation extends LightningElement { @track accountName = ''; @track industry = ''; isLoading = false; handleNameChange(event) { this.accountName = event.target.value; } handleIndustryChange(event) { this.industry = event.target.value; } async handleCreate() { if (!this.accountName) return; this.isLoading = true; try { const result = await executeMutation(CREATE_ACCOUNT, { variables: { name: this.accountName, industry: this.industry || null } }); const newRecord = result.data.uiapi.AccountCreate.Record; this.showToast('Success', `Account "${newRecord.Name.value}" created`, 'success'); this.resetForm(); } catch (error) { this.handleError(error); } finally { this.isLoading = false; } } async handleUpdate(accountId, newName) { try { const result = await executeMutation(UPDATE_ACCOUNT, { variables: { id: accountId, name: newName } }); this.showToast('Success', 'Account updated', 'success'); return result.data.uiapi.AccountUpdate.Record; } catch (error) { this.handleError(error); } } async handleDelete(accountId) { try { await executeMutation(DELETE_ACCOUNT, { variables: { id: accountId } }); this.showToast('Success', 'Account deleted', 'success'); } catch (error) { this.handleError(error); } } handleError(error) { const message = error.graphQLErrors ? error.graphQLErrors.map(e => e.message).join(', ') : error.message || 'Unknown error'; this.showToast('Error', message, 'error'); } showToast(title, message, variant) { this.dispatchEvent(new ShowToastEvent({ title, message, variant })); } resetForm() { this.accountName = ''; this.industry = ''; } } ``` ### GraphQL Mutation Operations | Operation | Mutation Type | Notes | |-----------|---------------|-------| | **Create** | `{Object}Create` | Can request fields from newly created record | | **Update** | `{Object}Update` | Cannot query fields in same request | | **Delete** | `{Object}Delete` | Cannot query fields in same request | ### allOrNone Parameter Control transaction behavior with `allOrNone` (default: `true`): ```javascript const BATCH_CREATE = gql` mutation BatchCreate($allOrNone: Boolean = true) { uiapi(allOrNone: $allOrNone) { acc1: AccountCreate(input: { Account: { Name: "Account 1" } }) { Record { Id } } acc2: AccountCreate(input: { Account: { Name: "Account 2" } }) { Record { Id } } } } `; // If allOrNone=true: All rollback if any fails // If allOrNone=false: Only failed operations rollback ``` --- ## Modal Component Pattern Based on [James Simone's composable modal pattern](https://www.jamessimone.net/blog/joys-of-apex/lwc-composable-modal/). ```javascript // composableModal.js import { LightningElement, api } from 'lwc'; const OUTER_MODAL_CLASS = 'outerModalContent'; export default class ComposableModal extends LightningElement { @api modalHeader; @api modalTagline; @api modalSaveHandler; _isOpen = false; _focusableElements = []; @api toggleModal() { this._isOpen = !this._isOpen; if (this._isOpen) { this._focusableElements = [...this.querySelectorAll('.focusable')]; this._focusFirstElement(); window.addEventListener('keyup', this._handleKeyUp); } else { window.removeEventListener('keyup', this._handleKeyUp); } } get modalAriaHidden() { return !this._isOpen; } get modalClass() { return this._isOpen ? 'slds-modal slds-visible slds-fade-in-open' : 'slds-modal slds-hidden'; } get backdropClass() { return this._isOpen ? 'slds-backdrop slds-backdrop_open' : 'slds-backdrop'; } _handleKeyUp = (event) => { if (event.code === 'Escape') { this.toggleModal(); } else if (event.code === 'Tab') { this._handleTabNavigation(event); } } _handleTabNavigation(event) { // Focus trap logic - keep focus within modal const activeEl = this.template.activeElement; const lastIndex = this._focusableElements.length - 1; const currentIndex = this._focusableElements.indexOf(activeEl); if (event.shiftKey && currentIndex === 0) { this._focusableElements[lastIndex]?.focus(); } else if (!event.shiftKey && currentIndex === lastIndex) { this._focusFirstElement(); } } _focusFirstElement() { if (this._focusableElements.length > 0) { this._focusableElements[0].focus(); } } handleBackdropClick(event) { if (event.target.classList.contains(OUTER_MODAL_CLASS)) { this.toggleModal(); } } handleSave() { if (this.modalSaveHandler) { this.modalSaveHandler(); } this.toggleModal(); } disconnectedCallback() { window.removeEventListener('keyup', this._handleKeyUp); } } ``` ```html ``` --- ## Record Picker Pattern ```javascript // recordPicker.js import { LightningElement, api } from 'lwc'; export default class RecordPicker extends LightningElement { @api label = 'Select Record'; @api objectApiName = 'Account'; @api placeholder = 'Search...'; @api required = false; @api multiSelect = false; _selectedIds = []; @api get value() { return this.multiSelect ? this._selectedIds : this._selectedIds[0]; } set value(val) { this._selectedIds = Array.isArray(val) ? val : val ? [val] : []; } handleChange(event) { const recordId = event.detail.recordId; if (this.multiSelect) { if (!this._selectedIds.includes(recordId)) { this._selectedIds = [...this._selectedIds, recordId]; } } else { this._selectedIds = recordId ? [recordId] : []; } this.dispatchEvent(new CustomEvent('select', { detail: { recordId: this.value, recordIds: this._selectedIds } })); } handleRemove(event) { const idToRemove = event.target.dataset.id; this._selectedIds = this._selectedIds.filter(id => id !== idToRemove); this.dispatchEvent(new CustomEvent('select', { detail: { recordIds: this._selectedIds } })); } } ``` ```html ``` --- ## Workspace API Pattern ```javascript // workspaceTabManager.js import { LightningElement, wire } from 'lwc'; import { IsConsoleNavigation, getFocusedTabInfo, openTab, closeTab, setTabLabel, setTabIcon, refreshTab } from 'lightning/platformWorkspaceApi'; export default class WorkspaceTabManager extends LightningElement { @wire(IsConsoleNavigation) isConsole; async openRecordTab(recordId, objectApiName) { if (!this.isConsole) return; await openTab({ recordId, focus: true, icon: `standard:${objectApiName.toLowerCase()}`, label: 'Loading...' }); } async openSubtab(parentTabId, recordId) { if (!this.isConsole) return; await openTab({ parentTabId, recordId, focus: true }); } async getCurrentTabInfo() { if (!this.isConsole) return null; return await getFocusedTabInfo(); } async updateTabLabel(tabId, label) { if (!this.isConsole) return; await setTabLabel(tabId, label); } async updateTabIcon(tabId, iconName) { if (!this.isConsole) return; await setTabIcon(tabId, iconName); } async refreshCurrentTab() { if (!this.isConsole) return; const tabInfo = await getFocusedTabInfo(); await refreshTab(tabInfo.tabId); } async closeCurrentTab() { if (!this.isConsole) return; const tabInfo = await getFocusedTabInfo(); await closeTab(tabInfo.tabId); } } ``` --- ## Parent-Child Communication ```javascript // parent.js import { LightningElement } from 'lwc'; export default class Parent extends LightningElement { selectedAccountId; handleAccountSelected(event) { this.selectedAccountId = event.detail.accountId; } } ``` ```html ``` ```javascript // child.js (accountList) import { LightningElement } from 'lwc'; export default class AccountList extends LightningElement { handleRowAction(event) { const accountId = event.detail.row.Id; // Dispatch event to parent this.dispatchEvent(new CustomEvent('accountselected', { detail: { accountId }, bubbles: true, composed: false // Don't cross shadow DOM boundaries })); } } ``` --- ## Sibling Communication (via Parent) When two child components need to communicate but share the same parent, use the **parent as middleware**. This is the recommended pattern for master-detail UIs. ```text ┌─────────────────────────────────────────────────────────────────────┐ │ SIBLING COMMUNICATION FLOW │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────┐ │ │ │ Parent │ ← Manages state │ │ └────┬─────┘ │ │ ┌─────────┴─────────┐ │ │ │ │ │ │ CustomEvent @api property │ │ (up) (down) │ │ │ │ │ │ ┌─────┴─────┐ ┌─────┴─────┐ │ │ │ Child A │ │ Child B │ │ │ │ (List) │ │ (Detail) │ │ │ └───────────┘ └───────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────┘ ``` **The flow**: 1. **Child A** dispatches a custom event (e.g., user selects an account) 2. **Parent** catches the event and updates its state 3. **Parent** passes data to **Child B** via `@api` property ### Complete Example: Account List → Account Detail ```javascript // accountContainer.js - Parent orchestrates communication between siblings import { LightningElement } from 'lwc'; export default class AccountContainer extends LightningElement { // State managed at parent level selectedAccountId; selectedAccountName; // Child A (accountList) fires this event handleAccountSelect(event) { this.selectedAccountId = event.detail.accountId; this.selectedAccountName = event.detail.accountName; } // Clear selection (triggered by Child B) handleClearSelection() { this.selectedAccountId = null; this.selectedAccountName = null; } get hasSelection() { return !!this.selectedAccountId; } } ``` ```html ``` ```javascript // accountList.js - Child A: Dispatches events UP to parent import { LightningElement, api, wire } from 'lwc'; import getAccounts from '@salesforce/apex/AccountController.getAccounts'; export default class AccountList extends LightningElement { @api selectedId; // Highlight selected row (from parent) accounts; error; @wire(getAccounts) wiredAccounts({ data, error }) { if (data) { this.accounts = data; this.error = undefined; } else if (error) { this.error = error; this.accounts = undefined; } } handleRowClick(event) { const accountId = event.currentTarget.dataset.id; const accountName = event.currentTarget.dataset.name; // Dispatch event to parent (not bubbles - parent listens directly) this.dispatchEvent(new CustomEvent('accountselect', { detail: { accountId, accountName } })); } // Computed: Check if row should be highlighted getRowClass(accountId) { return accountId === this.selectedId ? 'slds-item slds-is-selected' : 'slds-item'; } } ``` ```javascript // accountDetail.js - Child B: Receives data via @api from parent import { LightningElement, api, wire } from 'lwc'; import { getRecord, getFieldValue } from 'lightning/uiRecordApi'; import INDUSTRY_FIELD from '@salesforce/schema/Account.Industry'; import REVENUE_FIELD from '@salesforce/schema/Account.AnnualRevenue'; const FIELDS = [INDUSTRY_FIELD, REVENUE_FIELD]; export default class AccountDetail extends LightningElement { @api accountId; // Received from parent @api accountName; // Received from parent @wire(getRecord, { recordId: '$accountId', fields: FIELDS }) account; get industry() { return getFieldValue(this.account.data, INDUSTRY_FIELD); } get revenue() { return getFieldValue(this.account.data, REVENUE_FIELD); } get isLoading() { return !this.account.data && !this.account.error; } handleClose() { // Dispatch event back to parent to clear selection this.dispatchEvent(new CustomEvent('clearselection')); } } ``` ### When to Use Sibling Pattern vs LMS | Scenario | Sibling Pattern | LMS | |----------|-----------------|-----| | Components share same parent | ✅ Recommended | ❌ Overkill | | State is simple (1-2 values) | ✅ | ❌ | | Need bidirectional updates | ✅ | ✅ | | Components in different DOM trees | ❌ | ✅ Required | | Cross-framework (LWC ↔ Aura) | ❌ | ✅ Required | | Many consumers need same data | ❌ Consider LMS | ✅ | | Component hierarchy is deep (4+ levels) | ❌ Consider LMS | ✅ | **Rule of thumb**: If components share a parent and data flow is simple, use sibling pattern. If components are "far apart" in the DOM or you need pub/sub semantics, use LMS. --- ## Navigation Patterns ```javascript // navigator.js import { LightningElement } from 'lwc'; import { NavigationMixin } from 'lightning/navigation'; export default class Navigator extends NavigationMixin(LightningElement) { navigateToRecord(recordId, objectApiName = 'Account') { this[NavigationMixin.Navigate]({ type: 'standard__recordPage', attributes: { recordId, objectApiName, actionName: 'view' } }); } navigateToList(objectApiName, filterName = 'Recent') { this[NavigationMixin.Navigate]({ type: 'standard__objectPage', attributes: { objectApiName, actionName: 'list' }, state: { filterName } }); } navigateToNewRecord(objectApiName, defaultValues = {}) { this[NavigationMixin.Navigate]({ type: 'standard__objectPage', attributes: { objectApiName, actionName: 'new' }, state: { defaultFieldValues: Object.entries(defaultValues) .map(([k, v]) => `${k}=${encodeURIComponent(v)}`) .join(',') } }); } navigateToRelatedList(recordId, relationshipApiName) { this[NavigationMixin.Navigate]({ type: 'standard__recordRelationshipPage', attributes: { recordId, relationshipApiName, actionName: 'view' } }); } navigateToNamedPage(pageName, params = {}) { this[NavigationMixin.Navigate]({ type: 'standard__namedPage', attributes: { pageName }, state: params }); } } ``` --- ## TypeScript Patterns ### TypeScript Component Pattern ```typescript // accountList.ts import { LightningElement, api, wire, track } from 'lwc'; import { getRecord, getFieldValue } from 'lightning/uiRecordApi'; import getAccounts from '@salesforce/apex/AccountController.getAccounts'; import ACCOUNT_NAME_FIELD from '@salesforce/schema/Account.Name'; // Define interfaces for type safety interface AccountRecord { Id: string; Name: string; Industry?: string; AnnualRevenue?: number; } interface WireResult { data?: T; error?: Error; } export default class AccountList extends LightningElement { // Typed @api properties @api recordId: string | undefined; @api get maxRecords(): number { return this._maxRecords; } set maxRecords(value: number) { this._maxRecords = value; } // Typed @track properties @track private _accounts: AccountRecord[] = []; @track private _error: string | null = null; private _maxRecords: number = 10; private _wiredResult: WireResult | undefined; // Typed wire service @wire(getAccounts, { maxRecords: '$maxRecords' }) wiredAccounts(result: WireResult): void { this._wiredResult = result; const { data, error } = result; if (data) { this._accounts = data; this._error = null; } else if (error) { this._error = this.reduceErrors(error); this._accounts = []; } } // Typed getters get accounts(): AccountRecord[] { return this._accounts; } get hasAccounts(): boolean { return this._accounts.length > 0; } // Typed event handlers handleSelect(event: CustomEvent<{ accountId: string }>): void { const { accountId } = event.detail; this.dispatchEvent(new CustomEvent('accountselected', { detail: { accountId }, bubbles: true, composed: true })); } // Typed utility methods private reduceErrors(error: Error | Error[]): string { const errors = Array.isArray(error) ? error : [error]; return errors .filter((e): e is Error => e !== null) .map(e => e.message || 'Unknown error') .join('; '); } } ``` ### TypeScript Jest Test Pattern ```typescript // accountList.test.ts import { createElement, LightningElement } from 'lwc'; import AccountList from 'c/accountList'; import getAccounts from '@salesforce/apex/AccountController.getAccounts'; // Type definitions for tests interface AccountRecord { Id: string; Name: string; Industry?: string; } // Mock Apex jest.mock( '@salesforce/apex/AccountController.getAccounts', () => ({ default: jest.fn() }), { virtual: true } ); const MOCK_ACCOUNTS: AccountRecord[] = [ { Id: '001xx000003DGQ', Name: 'Acme Corp', Industry: 'Technology' } ]; describe('c-account-list', () => { let element: LightningElement & { maxRecords?: number }; afterEach(() => { while (document.body.firstChild) { document.body.removeChild(document.body.firstChild); } jest.clearAllMocks(); }); it('displays accounts after data loads', async () => { (getAccounts as jest.Mock).mockResolvedValue(MOCK_ACCOUNTS); element = createElement('c-account-list', { is: AccountList }); document.body.appendChild(element); await Promise.resolve(); const items = element.shadowRoot?.querySelectorAll('.slds-item'); expect(items?.length).toBe(MOCK_ACCOUNTS.length); }); }); ``` ### TypeScript Features for LWC | Feature | LWC Support | Notes | |---------|-------------|-------| | **Interface definitions** | ✅ | Define shapes for records, events, props | | **Typed @api properties** | ✅ | Getter/setter patterns with types | | **Typed @wire results** | ✅ | Generic `WireResult` pattern | | **Typed event handlers** | ✅ | `CustomEvent` for event detail typing | | **Private class fields** | ✅ | Use `private` keyword | | **Strict null checking** | ✅ | Optional chaining `?.` and nullish coalescing `??` | --- ## Apex Controller Patterns ### Cacheable Methods (for @wire) ```apex public with sharing class LwcController { @AuraEnabled(cacheable=true) public static List getAccounts(String searchTerm) { String searchKey = '%' + String.escapeSingleQuotes(searchTerm) + '%'; return [ SELECT Id, Name, Industry, AnnualRevenue FROM Account WHERE Name LIKE :searchKey WITH SECURITY_ENFORCED ORDER BY Name LIMIT 50 ]; } @AuraEnabled(cacheable=true) public static List getIndustryOptions() { List options = new List(); Schema.DescribeFieldResult fieldResult = Account.Industry.getDescribe(); for (Schema.PicklistEntry entry : fieldResult.getPicklistValues()) { if (entry.isActive()) { options.add(new PicklistOption(entry.getLabel(), entry.getValue())); } } return options; } public class PicklistOption { @AuraEnabled public String label; @AuraEnabled public String value; public PicklistOption(String label, String value) { this.label = label; this.value = value; } } } ``` ### Non-Cacheable Methods (for DML) ```apex @AuraEnabled public static Account createAccount(String accountJson) { Account acc = (Account) JSON.deserialize(accountJson, Account.class); // FLS check SObjectAccessDecision decision = Security.stripInaccessible( AccessType.CREATABLE, new List{ acc } ); insert decision.getRecords(); return (Account) decision.getRecords()[0]; } @AuraEnabled public static void deleteAccounts(List accountIds) { if (accountIds == null || accountIds.isEmpty()) { throw new AuraHandledException('No accounts to delete'); } List toDelete = [ SELECT Id FROM Account WHERE Id IN :accountIds WITH SECURITY_ENFORCED ]; delete toDelete; } ``` ### Error Handling Pattern ```apex @AuraEnabled public static List getContactsWithErrorHandling(Id accountId) { try { if (accountId == null) { throw new AuraHandledException('Account ID is required'); } List contacts = [ SELECT Id, Name, Email, Phone FROM Contact WHERE AccountId = :accountId WITH SECURITY_ENFORCED ORDER BY Name LIMIT 100 ]; return contacts; } catch (Exception e) { throw new AuraHandledException('Error fetching contacts: ' + e.getMessage()); } } ``` --- ## Related Resources - [lms-guide.md](lms-guide.md) - Lightning Message Service deep dive - [jest-testing.md](jest-testing.md) - Advanced testing patterns - [accessibility-guide.md](accessibility-guide.md) - WCAG compliance - [performance-guide.md](performance-guide.md) - Optimization techniques --- ## External References - [PICKLES Framework](https://www.salesforceben.com/the-ideal-framework-for-architecting-salesforce-lightning-web-components/) — David Picksley, Third Eye Consulting - [LWC Recipes (GitHub)](https://github.com/trailheadapps/lwc-recipes) - [James Simone - Composable Modal](https://www.jamessimone.net/blog/joys-of-apex/lwc-composable-modal/)