# 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. ```text ┌─────────────────────────────────────────────────────────────────────────────┐ │ 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 ```javascript 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) ```javascript 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`) ```javascript /** * 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 ```javascript // 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 ```bash # If using npm in LWC project npm install @lwc/state ``` ### Core Concepts #### Atoms (Primitive State) ```javascript 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) ```javascript 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) ```javascript 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 ```javascript 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. ```javascript 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. ```javascript 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 ```javascript 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 ```javascript // DON'T - won't trigger reactivity this.user.name = 'New Name'; this.items.push(newItem); ``` ### GOOD: Create New References ```javascript // DO - triggers reactivity this.user = { ...this.user, name: 'New Name' }; this.items = [...this.items, newItem]; ``` ### BAD: Heavy Computation in Getters ```javascript // 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 ```javascript _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 ```javascript // DON'T - memory leak connectedCallback() { store.subscribe('data', (data) => this.data = data); } ``` ### GOOD: Clean Up Subscriptions ```javascript // DO - proper cleanup _unsubscribe; connectedCallback() { this._unsubscribe = store.subscribe('data', (data) => this.data = data); } disconnectedCallback() { if (this._unsubscribe) { this._unsubscribe(); } } ``` --- ## Best Practices Summary ```text ┌─────────────────────────────────────────────────────────────────────────────┐ │ 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 │ │ │ └─────────────────────────────────────────────────────────────────────────────┘ ``` --- ## Related Documentation - [SKILL.md](../SKILL.md) - PICKLES Framework overview - [lwc-best-practices.md](lwc-best-practices.md) - General LWC patterns - [triangle-pattern.md](triangle-pattern.md) - Component composition --- ## Source > **References**: > - [Mastering State Management in LWC using @lwc/state](https://salesforcediaries.com/2025/11/26/mastering-state-management-in-lwc-using-lwc-state/) - Salesforce Diaries > - [Platform State Managers in LWC](https://salesforcediaries.com/2025/11/26/platform-state-managers-in-lwc/) - Salesforce Diaries