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:

  • Reactive Atoms: Containers that hold state and notify subscribers when values change
  • Computed Values: Derived state that automatically recalculates when dependencies change
  • Actions: Functions that safely update state
  • Automatic Re-renders: Components automatically update when state changes

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:

  • 4-step wizard: Personal Info → Employment → Financial → Review & Submit
  • Progress tracking: Show completion percentage across different pages
  • Data persistence: Preserve data across page refreshes
  • Cross-page communication: Status tracker on a different page should update in real-time
  • Validation: Enforce required fields before navigation
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 relationships, but as applications grow more complex, managing LWC state across multiple components becomes increasingly difficult.

Traditional approach problems

  • Prop drilling through multiple component levels
  • Complex event chains for upward communication
  • No built-in way to share state across pages
  • Manual localStorage management
  • Difficult to keep UI in sync

@lwc/state solution

  • Single source of truth (global store)
  • Automatic reactivity – components update when state changes
  • Built-in persistence patterns
  • No prop drilling or event chains needed
  • Easy to scale

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:

  • Atoms (atom()): Reactive containers that hold state. When you update an atom with setAtom(), all components watching that atom automatically re-render.
  • Computed Values (computed()): Derived state that automatically recalculates when dependencies change. In our example, monthlyPayment recalculates whenever application.loanDetails changes.
  • Factory Pattern with Singleton: We export both the factory function (createLoanApplicationStore) and a pre-created singleton instance (loanApplicationStore). This follows the official Salesforce pattern and provides flexibility:
    • Use the singleton for global shared state (our use case)
    • Use the factory to create independent instances if needed
    • Use with fromContext() for hierarchical state management
  • Persistence: We integrate localStorage to save state on every update and restore it on initialization.

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:

  • Import Store Directly: loanStore = loanApplicationStore – each component accesses the global singleton
  • Read with Getters: get firstName() returns value from store
  • Write with Actions: this.loanStore.value.updatePersonalInfo() updates store
  • Automatic Reactivity: When store updates, all getters re-run and UI re-renders in ALL components watching the store
  • No Prop Drilling: Components anywhere in the app can access the store without it being passed down

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:

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 relationships, but as applications grow more complex, managing LWC state across multiple components becomes increasingly difficult.

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

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 relationships, but as applications grow more complex, managing LWC state across multiple components becomes increasingly difficult.
// 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

One comment

Leave a Reply