# 186 Component Patterns 1c9a889b

> LWC Component Patterns

- Skill: `tools-only/186-component-patterns-1c9a889b` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add tools-only/186-component-patterns-1c9a889b`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tools-only/186-component-patterns-1c9a889b/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: tools-only (https://skillmd.com/u/tools-only)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/tools-only/186-component-patterns-1c9a889b

---

# 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<Account> 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

```
┌─────────────────────────────────────────────────────────────────────────────────────┐
│                    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

```
                    ┌─────────────────────────────┐
                    │   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
<!-- accountCard.html -->
<template>
    <template lwc:if={isLoading}>
        <lightning-spinner alternative-text="Loading"></lightning-spinner>
    </template>
    <template lwc:if={account.data}>
        <div class="slds-box slds-theme_default">
            <h2 class="slds-text-heading_medium">{name}</h2>
            <p class="slds-text-color_weak">{industry}</p>
        </div>
    </template>
    <template lwc:if={account.error}>
        <p class="slds-text-color_error">{account.error.body.message}</p>
    </template>
</template>
```

### 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
<!-- composableModal.html -->
<template>
    <!-- Backdrop -->
    <div class={backdropClass}></div>

    <!-- Modal -->
    <div class={modalClass}
         role="dialog"
         aria-modal="true"
         aria-hidden={modalAriaHidden}
         aria-labelledby="modal-heading">

        <div class="slds-modal__container outerModalContent"
             onclick={handleBackdropClick}>

            <div class="slds-modal__content slds-p-around_medium">
                <!-- Header -->
                <template lwc:if={modalHeader}>
                    <h2 id="modal-heading" class="slds-text-heading_medium">
                        {modalHeader}
                    </h2>
                </template>
                <template lwc:if={modalTagline}>
                    <p class="slds-m-top_x-small slds-text-color_weak">
                        {modalTagline}
                    </p>
                </template>

                <!-- Slotted Content -->
                <div class="slds-m-top_medium">
                    <slot name="modalContent"></slot>
                </div>

                <!-- Footer -->
                <div class="slds-m-top_medium slds-text-align_right">
                    <lightning-button
                        label="Cancel"
                        onclick={toggleModal}
                        class="slds-m-right_x-small focusable">
                    </lightning-button>
                    <lightning-button
                        variant="brand"
                        label="Save"
                        onclick={handleSave}
                        class="focusable">
                    </lightning-button>
                </div>
            </div>
        </div>
    </div>

    <!-- Hidden background content -->
    <div aria-hidden={_isOpen}>
        <slot name="body"></slot>
    </div>
</template>
```

---

## 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
<!-- recordPicker.html -->
<template>
    <lightning-record-picker
        label={label}
        placeholder={placeholder}
        object-api-name={objectApiName}
        onchange={handleChange}
        required={required}>
    </lightning-record-picker>

    <template lwc:if={multiSelect}>
        <div class="slds-m-top_x-small">
            <template for:each={_selectedIds} for:item="id">
                <lightning-pill
                    key={id}
                    label={id}
                    data-id={id}
                    onremove={handleRemove}>
                </lightning-pill>
            </template>
        </div>
    </template>
</template>
```

---

## 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
<!-- parent.html -->
<template>
    <c-account-list onaccountselected={handleAccountSelected}></c-account-list>
    <template lwc:if={selectedAccountId}>
        <c-account-detail account-id={selectedAccountId}></c-account-detail>
    </template>
</template>
```

```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.

```
┌─────────────────────────────────────────────────────────────────────┐
│                    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
<!-- accountContainer.html -->
<template>
    <div class="slds-grid slds-gutters">
        <!-- Child A: Account List -->
        <div class="slds-col slds-size_1-of-2">
            <c-account-list
                onaccountselect={handleAccountSelect}
                selected-id={selectedAccountId}>
            </c-account-list>
        </div>

        <!-- Child B: Account Detail (receives data via @api) -->
        <div class="slds-col slds-size_1-of-2">
            <template lwc:if={hasSelection}>
                <c-account-detail
                    account-id={selectedAccountId}
                    account-name={selectedAccountName}
                    onclearselection={handleClearSelection}>
                </c-account-detail>
            </template>
            <template lwc:else>
                <div class="slds-box slds-theme_shade">
                    Select an account to view details
                </div>
            </template>
        </div>
    </div>
</template>
```

```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<T> {
    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<AccountRecord[]> | undefined;

    // Typed wire service
    @wire(getAccounts, { maxRecords: '$maxRecords' })
    wiredAccounts(result: WireResult<AccountRecord[]>): 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<T>` pattern |
| **Typed event handlers** | ✅ | `CustomEvent<T>` 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 Li

…(truncated)
