afv-library/skills/experience-lwc-generate/references/state-management.md

20 KiB

LWC State Management

Modern state management patterns for Lightning Web Components using @lwc/state and Platform State Managers

Overview

LWC state management has evolved beyond simple reactive properties. This guide covers modern patterns for managing complex state across components, including the @lwc/state library and Salesforce Platform State Managers.

┌─────────────────────────────────────────────────────────────────────────────┐
│                    STATE MANAGEMENT SPECTRUM                                 │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  SIMPLE ◄────────────────────────────────────────────────────────► COMPLEX │
│                                                                             │
│  @track/@api      Singleton Store      @lwc/state       Platform State     │
│  (Component)      (Cross-Component)    (Full Library)   (Record/Layout)    │
│                                                                             │
│  ┌─────────┐      ┌─────────────┐      ┌────────────┐   ┌───────────────┐  │
│  │ Single  │      │ Shared      │      │ Atoms +    │   │ Record Data + │  │
│  │ Component│      │ Across      │      │ Computed + │   │ Layout State │  │
│  │ State   │      │ Components  │      │ Actions    │   │ (Platform)   │  │
│  └─────────┘      └─────────────┘      └────────────┘   └───────────────┘  │
│                                                                             │
│  Use when:         Use when:            Use when:        Use when:          │
│  - Local state     - Shared cart        - Complex UI     - Record pages     │
│  - Form fields     - User prefs         - Async state    - Flexipages       │
│  - UI toggles      - Cached data        - Derived data   - Tab persistence  │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

When to Use Each Pattern

Pattern Complexity Scope Use Case
@track / reactive properties Low Component Form inputs, toggles, local UI state
Singleton Store Medium Cross-component Shopping cart, filters, user preferences
@lwc/state Medium-High Cross-component Complex forms, async state, computed values
Platform State Managers High Page/Record Record pages, layout-aware components

Pattern 1: Reactive Properties (Component-Level)

Standard Reactivity

import { LightningElement } from 'lwc';

export default class SimpleState extends LightningElement {
    // Reactive by default (primitive types and objects)
    counter = 0;
    isActive = false;
    user = { name: 'John', email: 'john@example.com' };

    // Object reassignment triggers reactivity
    updateUser() {
        // ✅ Works - new object reference
        this.user = { ...this.user, name: 'Jane' };

        // ❌ Won't trigger rerender - same reference
        // this.user.name = 'Jane';
    }

    increment() {
        this.counter++; // ✅ Primitive assignment is reactive
    }
}

Getters (Computed Properties)

export default class ComputedExample extends LightningElement {
    firstName = '';
    lastName = '';
    items = [];

    // Computed property - recalculates when dependencies change
    get fullName() {
        return `${this.firstName} ${this.lastName}`.trim();
    }

    get hasItems() {
        return this.items.length > 0;
    }

    get totalPrice() {
        return this.items.reduce((sum, item) => sum + item.price, 0);
    }

    get formattedPrice() {
        return new Intl.NumberFormat('en-US', {
            style: 'currency',
            currency: 'USD'
        }).format(this.totalPrice);
    }
}

Pattern 2: Singleton Store (Cross-Component State)

For sharing state across components without platform dependencies.

Store Module (store.js)

/**
 * Singleton store for cross-component state management.
 *
 * Usage:
 * import store from 'c/store';
 *
 * // Read state
 * const cart = store.getState('cart');
 *
 * // Update state
 * store.setState('cart', { items: [...cart.items, newItem] });
 *
 * // Subscribe to changes
 * store.subscribe('cart', (newCart) => { this.cart = newCart; });
 */

// Private state container
const state = new Map();

// Subscribers by key
const subscribers = new Map();

// Get current state for a key
function getState(key) {
    return state.get(key);
}

// Set state and notify subscribers
function setState(key, value) {
    const oldValue = state.get(key);
    state.set(key, value);

    // Notify all subscribers for this key
    const keySubscribers = subscribers.get(key) || [];
    keySubscribers.forEach(callback => {
        try {
            callback(value, oldValue);
        } catch (e) {
            console.error('Store subscriber error:', e);
        }
    });
}

// Subscribe to state changes
function subscribe(key, callback) {
    if (!subscribers.has(key)) {
        subscribers.set(key, []);
    }
    subscribers.get(key).push(callback);

    // Return unsubscribe function
    return () => {
        const keySubscribers = subscribers.get(key) || [];
        const index = keySubscribers.indexOf(callback);
        if (index > -1) {
            keySubscribers.splice(index, 1);
        }
    };
}

// Initialize state with default values
function initState(key, defaultValue) {
    if (!state.has(key)) {
        state.set(key, defaultValue);
    }
    return state.get(key);
}

// Clear state (useful for testing)
function clearState() {
    state.clear();
    subscribers.clear();
}

export default {
    getState,
    setState,
    subscribe,
    initState,
    clearState
};

Using the Store

// cartManager.js
import { LightningElement } from 'lwc';
import store from 'c/store';

export default class CartManager extends LightningElement {
    cart = { items: [], total: 0 };
    unsubscribe;

    connectedCallback() {
        // Initialize cart state
        this.cart = store.initState('cart', { items: [], total: 0 });

        // Subscribe to cart changes from other components
        this.unsubscribe = store.subscribe('cart', (newCart) => {
            this.cart = newCart;
        });
    }

    disconnectedCallback() {
        // Clean up subscription
        if (this.unsubscribe) {
            this.unsubscribe();
        }
    }

    addItem(event) {
        const item = event.detail;
        const currentCart = store.getState('cart');

        const newCart = {
            items: [...currentCart.items, item],
            total: currentCart.total + item.price
        };

        store.setState('cart', newCart);
    }
}

Pattern 3: @lwc/state Library

The @lwc/state library provides reactive state primitives with automatic dependency tracking.

Installation

# If using npm in LWC project
npm install @lwc/state

Core Concepts

Atoms (Primitive State)

import { atom, computed } from '@lwc/state';

// Create atoms for primitive state
const countAtom = atom(0);
const nameAtom = atom('');
const itemsAtom = atom([]);

// Read value
console.log(countAtom.value); // 0

// Write value - triggers reactivity
countAtom.value = 5;

// Reset to initial value
countAtom.reset();

Computed (Derived State)

import { atom, computed } from '@lwc/state';

const priceAtom = atom(100);
const quantityAtom = atom(2);
const taxRateAtom = atom(0.08);

// Computed automatically recalculates when dependencies change
const subtotal = computed(() => priceAtom.value * quantityAtom.value);
const tax = computed(() => subtotal.value * taxRateAtom.value);
const total = computed(() => subtotal.value + tax.value);

console.log(total.value); // 216 (100 * 2 * 1.08)

// Update a dependency - all computed values update
priceAtom.value = 150;
console.log(total.value); // 324 (150 * 2 * 1.08)

Actions (State Mutations)

import { atom, action } from '@lwc/state';

const cartAtom = atom({ items: [], total: 0 });

// Actions encapsulate state mutations
const addToCart = action((item) => {
    const cart = cartAtom.value;
    cartAtom.value = {
        items: [...cart.items, item],
        total: cart.total + item.price
    };
});

const removeFromCart = action((itemId) => {
    const cart = cartAtom.value;
    const item = cart.items.find(i => i.id === itemId);
    cartAtom.value = {
        items: cart.items.filter(i => i.id !== itemId),
        total: cart.total - (item?.price || 0)
    };
});

const clearCart = action(() => {
    cartAtom.reset();
});

LWC Integration

import { LightningElement } from 'lwc';
import { atom, computed } from '@lwc/state';

// Define atoms outside component (singleton)
const searchTermAtom = atom('');
const resultsAtom = atom([]);
const isLoadingAtom = atom(false);

// Computed values
const hasResults = computed(() => resultsAtom.value.length > 0);
const resultCount = computed(() => resultsAtom.value.length);

export default class SearchComponent extends LightningElement {
    // Bind atoms to component for reactivity
    get searchTerm() {
        return searchTermAtom.value;
    }

    get results() {
        return resultsAtom.value;
    }

    get isLoading() {
        return isLoadingAtom.value;
    }

    get hasResults() {
        return hasResults.value;
    }

    handleSearchChange(event) {
        searchTermAtom.value = event.target.value;
        this.performSearch();
    }

    async performSearch() {
        if (searchTermAtom.value.length < 2) {
            resultsAtom.value = [];
            return;
        }

        isLoadingAtom.value = true;
        try {
            const results = await searchRecords({ term: searchTermAtom.value });
            resultsAtom.value = results;
        } finally {
            isLoadingAtom.value = false;
        }
    }
}

Pattern 4: Platform State Managers

Salesforce provides built-in state managers for record pages and layouts.

stateManagerRecord

Manages record data with automatic refresh and caching.

import { LightningElement, wire } from 'lwc';
import { stateManagerRecord } from 'lightning/stateManagerRecord';
import { getRecord } from 'lightning/uiRecordApi';

export default class RecordStateExample extends LightningElement {
    @api recordId;

    // Wire with state manager for enhanced caching
    @wire(stateManagerRecord, {
        recordId: '$recordId',
        fields: ['Account.Name', 'Account.Industry']
    })
    recordState;

    get accountName() {
        return this.recordState?.data?.fields?.Name?.value;
    }

    get industry() {
        return this.recordState?.data?.fields?.Industry?.value;
    }

    // State manager provides loading/error states
    get isLoading() {
        return this.recordState?.loading;
    }

    get hasError() {
        return !!this.recordState?.error;
    }
}

stateManagerLayout

Manages layout-aware state for flexipage components.

import { LightningElement, wire } from 'lwc';
import { stateManagerLayout } from 'lightning/stateManagerLayout';

export default class LayoutAwareComponent extends LightningElement {
    @api recordId;
    @api objectApiName;

    // Wire layout state manager
    @wire(stateManagerLayout, {
        recordId: '$recordId',
        objectApiName: '$objectApiName'
    })
    layoutState;

    get isCompact() {
        return this.layoutState?.density === 'compact';
    }

    get visibleFields() {
        return this.layoutState?.fields || [];
    }
}

Composing State Managers

import { LightningElement, wire } from 'lwc';
import { stateManagerRecord } from 'lightning/stateManagerRecord';
import { atom, computed } from '@lwc/state';

// Custom state atoms
const selectedTabAtom = atom('details');
const expandedSectionsAtom = atom(new Set(['overview']));

export default class ComposedStateComponent extends LightningElement {
    @api recordId;

    // Platform state for record data
    @wire(stateManagerRecord, {
        recordId: '$recordId',
        fields: ['Account.Name', 'Account.Type', 'Account.Industry']
    })
    recordState;

    // Custom state for UI
    get selectedTab() {
        return selectedTabAtom.value;
    }

    get expandedSections() {
        return expandedSectionsAtom.value;
    }

    isSectionExpanded(sectionId) {
        return expandedSectionsAtom.value.has(sectionId);
    }

    handleTabChange(event) {
        selectedTabAtom.value = event.detail.value;
    }

    handleSectionToggle(event) {
        const sectionId = event.target.dataset.section;
        const sections = new Set(expandedSectionsAtom.value);

        if (sections.has(sectionId)) {
            sections.delete(sectionId);
        } else {
            sections.add(sectionId);
        }

        expandedSectionsAtom.value = sections;
    }
}

Anti-Patterns to Avoid

BAD: Mutating Objects In Place

// DON'T - won't trigger reactivity
this.user.name = 'New Name';
this.items.push(newItem);

GOOD: Create New References

// DO - triggers reactivity
this.user = { ...this.user, name: 'New Name' };
this.items = [...this.items, newItem];

BAD: Heavy Computation in Getters

// DON'T - runs every render cycle
get expensiveComputation() {
    return this.items
        .map(item => complexTransform(item))
        .filter(item => complexFilter(item))
        .sort((a, b) => complexSort(a, b));
}

GOOD: Cache Computed Values

_cachedResult;
_lastItemsHash;

get optimizedComputation() {
    const currentHash = JSON.stringify(this.items);
    if (this._lastItemsHash !== currentHash) {
        this._cachedResult = this.items
            .map(item => complexTransform(item))
            .filter(item => complexFilter(item))
            .sort((a, b) => complexSort(a, b));
        this._lastItemsHash = currentHash;
    }
    return this._cachedResult;
}

BAD: Forgetting to Unsubscribe

// DON'T - memory leak
connectedCallback() {
    store.subscribe('data', (data) => this.data = data);
}

GOOD: Clean Up Subscriptions

// DO - proper cleanup
_unsubscribe;

connectedCallback() {
    this._unsubscribe = store.subscribe('data', (data) => this.data = data);
}

disconnectedCallback() {
    if (this._unsubscribe) {
        this._unsubscribe();
    }
}

Best Practices Summary

┌─────────────────────────────────────────────────────────────────────────────┐
│                    STATE MANAGEMENT BEST PRACTICES                           │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  CHOOSE THE RIGHT PATTERN                                                   │
│  ─────────────────────────────────────────────────────────────────────────  │
│  ✅ Start with simple reactive properties                                   │
│  ✅ Use singleton store for shared non-record state                         │
│  ✅ Use @lwc/state for complex derived state                                │
│  ✅ Use Platform State Managers on record pages                             │
│  ❌ Don't over-engineer simple components                                   │
│                                                                             │
│  REACTIVITY                                                                 │
│  ─────────────────────────────────────────────────────────────────────────  │
│  ✅ Create new object/array references for updates                          │
│  ✅ Use spread operator: { ...obj, newProp }                                │
│  ✅ Use getters for computed values                                         │
│  ❌ Don't mutate objects/arrays in place                                    │
│                                                                             │
│  PERFORMANCE                                                                │
│  ─────────────────────────────────────────────────────────────────────────  │
│  ✅ Cache expensive computations                                            │
│  ✅ Debounce rapid state updates                                            │
│  ✅ Clean up subscriptions in disconnectedCallback                          │
│  ❌ Don't do heavy computation in getters                                   │
│                                                                             │
│  TESTING                                                                    │
│  ─────────────────────────────────────────────────────────────────────────  │
│  ✅ Test state transitions explicitly                                       │
│  ✅ Verify subscription cleanup                                             │
│  ✅ Mock store for unit tests                                               │
│  ❌ Don't test implementation details                                       │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘


Source

References: