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

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:
- Store Reference:
loanStore = loanApplicationStoregives us access to the global state - Reactive Getters:
get currentStep()reads from store using.valueaccessor - 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:

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
@trackinstead) - Simple parent-child communication (use
@apiproperties) - 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
- Official Documentation: LWC State Management Guide
- GitHub Examples: forcedotcom/state-management
- Complete Code: https://github.com/SalesforceDiariesBySanket/simple-state-management-in-lwc
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

One comment