Skip to content
Back to skills

538 Lwc Best Practices 060d5383

ASecurity

This guide provides comprehensive best practices for building production-ready LWC components, organized around the **PICKLES Framework** and incorporating advanced patterns from industry experts. ---

  • 9 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 11, 2026
developmentjavascriptgojavareactexpressspringtestinggitapisecurity

Works with

  • cursor
  • cli
  • api

Security analysis

A100/100

Scanned October 11, 2026

npx -y skills add tools-only/X-Skills --skill 538-lwc-best-practices_060d5383 --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of 538 Lwc Best Practices 060d5383?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for 538 Lwc Best Practices 060d5383
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/tools-only-538-lwc-best-practices-060d5383/badge)](https://www.skillsdirectory.com/skills/tools-only-538-lwc-best-practices-060d5383)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

SKILL.md
# 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 (Salesforce Ben)](https://www.salesforceben.com/the-ideal-framework-for-architecting-salesforce-lightning-web-components/)

---

## 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
<!-- Compose components -->
<template>
    <c-page-header title="Accounts"></c-page-header>
    <c-account-filters onfilter={handleFilter}></c-account-filters>
    <c-account-list accounts={filteredAccounts}></c-account-list>
    <c-pagination total={totalCount} onpage={handlePage}></c-pagination>
</template>
```

### 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                        │   │
│   ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜   │
│                                                                  │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
```

---

## 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 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
<!-- Without lwc:spread (verbose) -->
<lightning-button
    label={buttonLabel}
    variant={buttonVariant}
    disabled={isDisabled}
    onclick={handleClick}>
</lightning-button>

<!-- With lwc:spread (dynamic) -->
<lightning-button lwc:spread={buttonAttributes} onclick={handleClick}></lightning-button>
```

```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
<lightning-input lwc:spread={inputAttributes} onchange={handleChange}></lightning-input>
```

#### Event Handlers with lwc:spread

**Important**: Event handlers must be bound separately, not spread:

```html
<!-- āœ… CORRECT: Event handler separate from spread -->
<lightning-button lwc:spread={buttonProps} onclick={handleClick}></lightning-button>

<!-- āŒ INCORRECT: onclick in spread object won't work -->
<!-- buttonProps = { label: 'Save', onclick: this.handleClick } -->
```

### 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
<!-- template.html -->
<template>
    <!-- Bind multiple event handlers dynamically -->
    <button lwc:on={eventHandlers}>Click Me</button>
</template>
```

#### 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
<!-- template.html -->
<template>
    <!-- Best of both worlds: dynamic props AND dynamic events -->
    <lightning-button
        lwc:spread={buttonAttributes}
        lwc:on={buttonEvents}>
    </lightning-button>
</template>
```

#### 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
<!-- childComponent.html -->
<template>
    <div lwc:on={resolvedHandlers}>
        <slot></slot>
    </div>
</template>
```

#### 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
<!-- BEFORE Spring '26: Required getters for any logic -->
<template>
    <!-- Simple property binding only -->
    <template lwc:if={isValid}>...</template>

    <!-- Complex conditions needed a getter -->
    <template lwc:if={showLoadingState}>...</template>
</template>
```

```javascript
// Required getter in JS
get showLoadingState() {
    return this.isLoading && this.items.length === 0;
}
```

```html
<!-- AFTER Spring '26 (Beta): Complex expressions in template -->
<template>
    <!-- Logical operators -->
    <template lwc:if={!isLoading && items.length > 0}>
        <c-item-list items={items}></c-item-list>
    </template>

    <!-- Optional chaining -->
    <template lwc:if={user?.permissions?.canEdit}>
        <lightning-button label="Edit"></lightning-button>
    </template>

    <!-- Arithmetic expressions -->
    <span class="slds-text-body_small">
        Total: ${total * taxRate}
    </span>

    <!-- Comparison operators -->
    <template lwc:if={items.length >= minItems}>
        <c-pagination></c-pagination>
    </template>
</template>
```

### 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
<!-- āœ… GOOD: Simple inline logic -->
<template lwc:if={!isLoading && hasData}>
    ...
</template>

<!-- āœ… GOOD: Optional chaining for safety -->
<span>{account?.Owner?.Name}</span>

<!-- āš ļø CAUTION: Keep expressions readable -->
<!-- If expression is long, consider a getter for maintainability -->
<template lwc:if={isEditable && hasPermission && !isLocked && status === 'draft'}>
    <!-- Consider: get canEdit() { return ...; } -->
</template>

<!-- āŒ AVOID: Side effects in expressions -->
<!-- Don't call methods that modify state -->
```

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

---

## Performance Optimization (PICKLES: Execution)

### Lifecycle Hook Guidance

| Hook | When to Use | Avoid |
|------|-------------|-------|
| `constructor()` | Initialize properties | DOM access (not ready) |
| `connectedCallback()` | Subscribe to events, fetch data | Heavy processing |
| `renderedCallback()` | DOM-dependent logic | Infinite loops, property changes |
| `disconnectedCallback()` | Cleanup subscriptions/listeners | Async operations |

### Lazy Loading

```html
<!-- Only render when needed -->
<template lwc:if={showDetails}>
    <c-expensive-component record-id={recordId}></c-expensive-component>
</template>
```

### Efficient Rendering

```javascript
// Bad: Creates new array every render
get filteredItems() {
    return this.items.filter(item => item.active);
}

// Good: Cache the result
_filteredItems;
_itemsHash;

get filteredItems() {
    const currentHash = JSON.stringify(this.items);
    if (currentHash !== this._itemsHash) {
        this._filteredItems = this.items.filter(item => item.active);
        this._itemsHash = currentHash;
    }
    return this._filteredItems;
}
```

### Virtual Scrolling

Use `lightning-datatable` with `enable-infinite-loading` for large datasets instead of rendering all items.

---

## Advanced Jest Testing Patterns

Based on [James Simone's advanced testing patterns](https://www.jamessimone.net/blog/joys-of-apex/advanced-lwc-jest-testing/).

### Render Cycle Helper

LWC re-rendering is asynchronous. Use this helper to document and await render cycles:

```javascript
// testUtils.js
export const runRenderingLifecycle = async (reasons = ['render']) => {
    while (reasons.length > 0) {
        await Promise.resolve(reasons.pop());
    }
};

// Usage in tests
it('updates after property change', async () => {
    const element = createElement('c-example', { is: Example });
    document.body.appendChild(element);

    element.greeting = 'new value';
    await runRenderingLifecycle(['property change', 'render']);

    expect(element.shadowRoot.querySelector('div').textContent).toBe('new value');
});
```

### Proxy Unboxing (Lightning Web Security)

Lightning Web Security proxifies objects. Unbox them for assertions:

```javascript
// LWS proxifies complex objects - unbox for comparison
const unboxedData = JSON.parse(JSON.stringify(component.data));
expect(unboxedData).toEqual(expectedData);
```

### DOM Cleanup Pattern

Clean up after each test to prevent state bleed:

```javascript
describe('c-my-component', () => {
    afterEach(() => {
        // Clean up DOM
        while (document.body.firstChild) {
            document.body.removeChild(document.body.firstChild);
        }
        jest.clearAllMocks();
    });
});
```

### ResizeObserver Polyfill

Some components use ResizeObserver. Add polyfill in jest.setup.js:

```javascript
// jest.setup.js
if (!window.ResizeObserver) {
    window.ResizeObserver = class ResizeObserver {
        constructor(callback) {
            this.callback = callback;
        }
        observe() {}
        unobserve() {}
        disconnect() {}
    };
}
```

### Mocking Apex Methods

```javascript
jest.mock('@salesforce/apex/MyController.getData', () => ({
    default: jest.fn()
}), { virtual: true });

// In test
import getData from '@salesforce/apex/MyController.getData';

it('displays data', async () => {
    getData.mockResolvedValue(MOCK_DATA);
    // ... test code
});
```

---

## Security Best Practices (PICKLES: Security)

### FLS Enforcement

```apex
// Always use SECURITY_ENFORCED or stripInaccessible
@AuraEnabled(cacheable=true)
public static List<Account> getAccounts() {
    return [SELECT Id, Name FROM Account WITH SECURITY_ENFORCED];
}

// For DML operations
SObjectAccessDecision decision = Security.stripInaccessible(
    AccessType.CREATABLE,
    records
);
insert decision.getRecords();
```

### Input Sanitization

```apex
// Apex should escape user input
String searchKey = '%' + String.escapeSingleQuotes(searchTerm) + '%';
```

### XSS Prevention

LWC automatically escapes content in templates. Never bypass this.

```html
<!-- Safe: LWC auto-escapes -->
<p>{userInput}</p>
```

---

## Accessibility (a11y)

### Required Practices

| Element | Requirement |
|---------|-------------|
| Buttons | `label` or `aria-label` |
| Icons | `alternative-text` |
| Form inputs | Associated `<label>` |
| Dynamic content | `aria-live` region |
| Loading states | `aria-busy="true"` |

### Keyboard Navigation

```javascript
handleKeyDown(event) {
    switch (event.key) {
        case 'Enter':
        case ' ':
            this.handleSelect(event);
            break;
        case 'Escape':
            this.handleClose();
            break;
        case 'ArrowDown':
            this.focusNext();
            event.preventDefault();
            break;
    }
}
```

### Focus Trap Pattern (for Modals)

Based on [James Simone's modal pattern](https://www.jamessimone.net/blog/joys-of-apex/lwc-composable-modal/):

```javascript
_focusableElements = [];

_onOpen() {
    // Collect focusable elements
    this._focusableElements = [
        ...this.querySelectorAll('.focusable'),
        ...this.template.querySelectorAll('lightning-button, button, [tabindex="0"]')
    ].filter(el => !el.disabled);

    // Focus first element
    this._focusableElements[0]?.focus();

    // Add ESC handler
    window.addEventListener('keyup', this._handleKeyUp);
}

_handleKeyUp = (event) => {
    if (event.code === 'Escape') {
        this.close();
    }
}

disconnectedCallback() {
    window.removeEventListener('keyup', this._handleKeyUp);
}
```

---

## SLDS 2 & Dark Mode

### Dark Mode Checklist

- [ ] No hardcoded hex colors (`#FFFFFF`, `#333333`)
- [ ] No hardcoded RGB/RGBA values
- [ ] All colors use CSS variables (`var(--slds-g-color-*)`)
- [ ] Fallback values provided for SLDS 1 compatibility
- [ ] Icons use SLDS utility icons (auto-adjust for dark mode)

### SLDS 1 → SLDS 2 Migration

```css
/* BEFORE (SLDS 1 - Deprecated) */
.my-card {
    background-color: #ffffff;
    color: #333333;
}

/* AFTER (SLDS 2 - Dark Mode Ready) */
.my-card {
    background-color: var(--slds-g-color-surface-container-1, #ffffff);
    color: var(--slds-g-color-on-surface, #181818);
}
```

### Key Global Styling Hooks

| Category | SLDS 2 Variable |
|----------|-----------------|
| Surface | `--slds-g-color-surface-1` to `-4` |
| Text | `--slds-g-color-on-surface` |
| Border | `--slds-g-color-border-1`, `-2` |
| Spacing | `--slds-g-spacing-0` to `-12` |

**Important**: `--slds-c-*` (component-level hooks) are NOT supported in SLDS 2 yet.

---

## Testing Checklist

### Unit Test Coverage

- [ ] Component renders without errors
- [ ] Data displays correctly when loaded
- [ ] Error state displays when fetch fails
- [ ] Empty state displays when no data
- [ ] Events dispatch with correct payload
- [ ] User interactions work correctly
- [ ] Loading states are shown/hidden appropriately

### Manual Testing

- [ ] Works in Lightning Experience
- [ ] Works in Salesforce Mobile
- [ ] Works in Experience Cloud (if targeted)
- [ ] Works in Dark Mode (SLDS 2)
- [ ] Keyboard navigation works
- [ ] Screen reader announces properly
- [ ] No console errors
- [ ] Performance acceptable with real data

---

## Common Mistakes

### 1. Modifying @api Properties

```javascript
// āŒ BAD
@api items;
handleClick() {
    this.items.push(newItem);  // Mutation!
}

// āœ… GOOD
handleClick() {
    this.items = [...this.items, newItem];
}
```

### 2. Forgetting to Clean Up

```javascript
// āŒ BAD: Memory leak
connectedCallback() {
    this.subscription = subscribe(...);
}

// āœ… GOOD
disconnectedCallback() {
    unsubscribe(this.subscription);
}
```

### 3. Wire with Non-Reactive Parameters

```javascript
// āŒ BAD
let recordId = '001xxx';
@wire(getRecord, { recordId: recordId })

// āœ… GOOD
@api recordId;
@wire(getRecord, { recordId: '$recordId' })
```

---

## Resources

- [PICKLES Framework (Salesforce Ben)](https://www.salesforceben.com/the-ideal-framework-for-architecting-salesforce-lightning-web-components/)
- [LWC Recipes (GitHub)](https://github.com/trailheadapps/lwc-recipes)
- [SLDS 2 Transition Guide](https://www.lightningdesignsystem.com/2e1ef8501/p/8184ad-transition-to-slds-2)
- [James Simone - Advanced Jest Testing](https://www.jamessimone.net/blog/joys-of-apex/advanced-lwc-jest-testing/)
- [James Simone - Composable Modal](https://www.jamessimone.net/blog/joys-of-apex/lwc-composable-modal/)
- [SLDS Styling Hooks](https://developer.salesforce.com/docs/platform/lwc/guide/create-components-css-custom-properties.html)

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.