Site icon Salesforce Diaries

Mastering State Management in LWC using @LWC/State

State management has been one of the biggest challenges in Lightning Web Components development. Passing data between components through properties and events works well for simple parent – child component relationships, but as applications grow more complex, managing LWC state across multiple components becomes increasingly difficult.

Manage State Across LWC Components with State Managers (Beta) – Salesforce’s official solution for reactive state management in LWC. This powerful library brings modern state management patterns to the Lightning platform, making it easier to build complex, data-driven applications.

In this comprehensive guide, I’ll walk you through building a real-world multi-step loan application using @lwc/state, demonstrating practical patterns that you can apply to your own projects.

What is @lwc/state?

@lwc/state is a reactive state management library for Lightning Web Components that provides:

Think of it as a lightweight, LWC-optimized version of modern state management libraries like Redux or MobX.

Problem Statement: Multi Step Loan Application in LWC

Let’s consider a real-world scenario: a multi-step loan application wizard with the following requirements:

Traditional approach problems

@lwc/state solution

Architecture Overview

┌─────────────────────────────────────────────────────────┐
│           loanApplicationStore (Singleton)               │
│  ┌─────────────────────────────────────────────────┐   │
│  │ Atoms:                                           │   │
│  │  • currentStep (1-4)                            │   │
│  │  • application (all form data)                  │   │
│  │                                                  │   │
│  │ Computed Values:                                │   │
│  │  • monthlyPayment (auto-calculated)             │   │
│  │  • isApplicationComplete (validation)           │   │
│  │                                                  │   │
│  │ Actions:                                        │   │
│  │  • updatePersonalInfo()                         │   │
│  │  • updateEmploymentInfo()                       │   │
│  │  • updateCurrentStep()                          │   │
│  │  • submitApplication()                          │   │
│  └─────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────┘
                         ▲
                         │ (import store)
        ┌────────────────┼────────────────┐
        │                │                │
  ┌──────────┐    ┌──────────┐    ┌──────────┐
  │  Wizard  │    │ Summary  │    │ Tracker  │
  │ (Step 1) │    │ (Page 2) │    │ (Page 3) │
  └──────────┘    └──────────┘    └──────────┘
   All components read from and write to the same store

Code – Loan Application

Find the full Implementation here : https://github.com/SalesforceDiariesBySanket/simple-state-management-in-lwc
Let’s understand our global state manager using defineState:

loanApplicationStore.js

// loanApplicationStore.js
import { defineState } from '@lwc/state';

// Helper functions for localStorage persistence
function loadFromStorage(key, defaultValue) {
    try {
        const stored = localStorage.getItem(key);
        return stored ? JSON.parse(stored) : defaultValue;
    } catch (e) {
        return defaultValue;
    }
}

function saveToStorage(key, value) {
    try {
        localStorage.setItem(key, JSON.stringify(value));
    } catch (e) {
        console.error('Failed to save to localStorage:', e);
    }
}

// Generate unique application ID
function generateApplicationId() {
    return 'LA-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
}

// Creator function for the loan application state manager
// Following the official Salesforce pattern
export const createLoanApplicationStore = defineState(({ atom, computed, setAtom }) => () => {
    
    // ATOMS: Reactive state containers
    // Load from localStorage on initialization
    const currentStep = atom(loadFromStorage('loanApp_currentStep', 1));
    
    const application = atom(loadFromStorage('loanApp_data', {
        id: generateApplicationId(),
        status: 'draft',
        personalInfo: {
            firstName: '',
            lastName: '',
            email: '',
            phone: '',
            ssn: '',
            dateOfBirth: '',
            address: {
                street: '',
                city: '',
                state: '',
                zipCode: ''
            }
        },
        employmentInfo: {
            employerName: '',
            jobTitle: '',
            employmentType: 'full-time',
            monthlyIncome: 0,
            yearsEmployed: 0,
            employerPhone: ''
        },
        financialInfo: {
            annualIncome: 0,
            creditScore: 0,
            existingDebts: 0,
            monthlyExpenses: 0
        },
        loanDetails: {
            loanAmount: 0,
            loanPurpose: '',
            loanTerm: 30,
            interestRate: 0
        },
        createdDate: new Date().toISOString(),
        lastModified: new Date().toISOString()
    }));

    // COMPUTED VALUES: Auto-calculated derived state
    // These recalculate automatically when dependencies change
    
    const monthlyPayment = computed([application], (app) => {
        const principal = app.loanDetails.loanAmount;
        const annualRate = app.loanDetails.interestRate / 100;
        const monthlyRate = annualRate / 12;
        const termMonths = app.loanDetails.loanTerm * 12;

        if (monthlyRate === 0) return principal / termMonths;

        return principal * (monthlyRate * Math.pow(1 + monthlyRate, termMonths)) /
               (Math.pow(1 + monthlyRate, termMonths) - 1);
    });

    const isApplicationComplete = computed([application], (app) => {
        const personal = app.personalInfo;
        const employment = app.employmentInfo;
        const financial = app.financialInfo;

        return !!(
            personal.firstName && personal.lastName &&
            employment.employerName &&
            financial.annualIncome > 0
        );
    });

    // ACTIONS: Functions to update state
    // Always use actions to modify atoms
    
    const updatePersonalInfo = (personalInfo) => {
        const updated = {
            ...application.value,
            personalInfo: { ...application.value.personalInfo, ...personalInfo },
            lastModified: new Date().toISOString()
        };
        setAtom(application, updated);
        saveToStorage('loanApp_data', updated); // Auto-persist
    };

    const updateEmploymentInfo = (employmentInfo) => {
        const updated = {
            ...application.value,
            employmentInfo: { ...application.value.employmentInfo, ...employmentInfo },
            lastModified: new Date().toISOString()
        };
        setAtom(application, updated);
        saveToStorage('loanApp_data', updated);
    };

    const updateFinancialInfo = (financialInfo) => {
        const updated = {
            ...application.value,
            financialInfo: { ...application.value.financialInfo, ...financialInfo },
            lastModified: new Date().toISOString()
        };
        setAtom(application, updated);
        saveToStorage('loanApp_data', updated);
    };

    const updateCurrentStep = (step) => {
        setAtom(currentStep, step);
        saveToStorage('loanApp_currentStep', step);
    };

    const submitApplication = () => {
        const updated = {
            ...application.value,
            status: 'submitted',
            lastModified: new Date().toISOString()
        };
        setAtom(application, updated);
        saveToStorage('loanApp_data', updated);
    };

    // Return the public API of the store
    return {
        // State
        currentStep,
        application,
        monthlyPayment,
        isApplicationComplete,
        
        // Actions
        updatePersonalInfo,
        updateEmploymentInfo,
        updateFinancialInfo,
        updateCurrentStep,
        submitApplication
    };
});

// For convenience, also export a singleton instance
// This allows both patterns: singleton for global state OR factory for multiple instances
export const loanApplicationStore = createLoanApplicationStore();

// Default export is the factory function (matches official pattern)
export default createLoanApplicationStore;

loanApplicationStore.js-meta.xml

<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>65.0</apiVersion>
    <isExposed>false</isExposed>
    <targets>
        <target>lightning__AppPage</target>
        <target>lightning__RecordPage</target>
        <target>lightning__HomePage</target>
    </targets>
</LightningComponentBundle>

Use this loanApplicationStore state manager in LWC

LoanApplicationWizard.js

What’s happening here:

  1. Store Reference: loanStore = loanApplicationStore gives us access to the global state
  2. Reactive Getters: get currentStep() reads from store using .value accessor
  3. State Updates: Calling this.loanStore.value.updateCurrentStep() updates the atom and triggers re-renders
import { LightningElement } from 'lwc';
import { loanApplicationStore } from 'c/loanApplicationStore';

export default class LoanApplicationWizard extends LightningElement {
    // Reference the global singleton store
    loanStore = loanApplicationStore;
    totalSteps = 4;

    // Read current step from store
    get currentStep() {
        return this.loanStore.value.currentStep || 1;
    }

    // find full code in github repo mentioned above

    // Navigation with validation
    handleNext() {
        if (!this.validateCurrentStep()) {
            return;
        }

        if (this.currentStep < this.totalSteps) {
            // Update state - all components watching currentStep will re-render
            this.loanStore.value.updateCurrentStep(this.currentStep + 1);
        } else {
            this.handleSubmit();
        }
    }

    handlePrevious() {
        if (this.currentStep > 1) {
            this.loanStore.value.updateCurrentStep(this.currentStep - 1);
        }
    }
    
    handleSubmit() {
        // Update status in global store
        this.loanStore.value.submitApplication();
        
        // Show success message, navigate, etc.
        this.dispatchEvent(
            new ShowToastEvent({
                title: 'Success',
                message: 'Application submitted successfully!',
                variant: 'success'
            })
        );
    }
}

Get value in Child LWC using loanApplicationStore

Each child component reads from and writes to the store:

One of the child component example below:

// personalInfoStep.js
import { LightningElement } from 'lwc';
import { loanApplicationStore } from 'c/loanApplicationStore';

export default class PersonalInfoStep extends LightningElement {
    // Import the global store directly - no props needed!
    loanStore = loanApplicationStore;

    // Reactive getters - automatically re-render when store changes
    get firstName() {
        return this.loanStore.value.application.personalInfo.firstName;
    }

    // Handle input changes
    handleInputChange(event) {
        const field = event.target.dataset.field;
        const value = event.target.value;

        // Update store - this will trigger re-renders in ALL components
        // watching the application atom (wizard, summary, tracker, etc.)
        this.loanStore.value.updatePersonalInfo({
            [field]: value
        });
    }
}

Pass Data to Cross-Page Components using LWC State Manager

Here’s where @lwc/state really shines – components on different pages can share state:

This summary component can be placed on a completely different Lightning page than the wizard. When users fill out the form on Page 1, the summary on Page 2 automatically updates in real-time! No events, no messaging channels – just reactive state.

Note: If you have these two pages opened in two different window, you need to refresh the summary page, If you place both component on same page, It gets auto updated

// loanApplicationSummary.js
import { LightningElement } from 'lwc';
import { loanApplicationStore } from 'c/loanApplicationStore';

export default class LoanApplicationSummary extends LightningElement {
    // Import store directly - no props needed!
    loanStore = loanApplicationStore;

    get applicationId() {
        return this.loanStore.value.application.id;
    }
}

This all happens automatically! No manual subscription management, no event chains, no prop drilling. Each component simply imports the global store singleton and accesses the data it needs.

Key Benefits

1. Single Source of Truth

All data lives in one place. No duplicate state, no confusion about which data is “correct”.

2. Automatic Reactivity

Components automatically re-render when dependent state changes. Just use getters that read from the store.

3. No Prop Drilling

// ❌ Old way: Pass data through 5 levels
<c-wizard>
  <c-step-container data={wizardData}>
    <c-step-group data={wizardData}>
      <c-step-form data={wizardData}>
        <c-input-field data={wizardData}> <!-- Finally! -->

// ✅ New way: Import store anywhere
import { loanApplicationStore } from 'c/loanApplicationStore';
loanStore = loanApplicationStore;

4. Cross-Page Communication

Components on different pages automatically stay in sync through the shared store.

5. Built-in Persistence

Integrate localStorage once in the store, and all components get persistence for free.

6. Computed Values

Business logic (like monthlyPayment) automatically recalculates when dependencies change.

7. Easy Testing

// Test the store independently
import { loanApplicationStore } from 'c/loanApplicationStore';

const store = loanApplicationStore();
store.value.updatePersonalInfo({ firstName: 'Test' });
expect(store.value.application.personalInfo.firstName).toBe('Test');

When to Use @lwc/state

✅ Use @lwc/state when:

  • Multiple components need access to shared data
  • You need cross-page state synchronization
  • You have complex computed values
  • You want automatic reactivity
  • You need a centralized place for business logic

❌ Don’t use @lwc/state when:

  • Component-local state is sufficient (use @track instead)
  • Simple parent-child communication (use @api properties)
  • One-time data passing (use events)

Conclusion

@lwc/state brings modern, reactive state management to Lightning Web Components. By centralizing state in a global store with atoms, computed values, and actions, we can build complex applications that are easier to reason about, test, and maintain.

Additional Resources

What’s Next?

In the next blog post, we’ll explore Platform State Manager Pattern – an advanced approach that uses nested state managers, and integration with Lightning Data Service for Salesforce record management.


Note: @lwc/state is currently in Beta (Developer Preview). Features may change before general availability. Always test thoroughly in sandbox environments.

Do you need help?

Are you stuck while working on this requirement? Do you want to get review of your solution? Now, you can book dedicated 1:1 with me on Lightning Web Component and Agentforce completely free.

GET IN TOUCH

Schedule a 1:1 Meeting with me

Exit mobile version