In our previous blog post, we explored the fundamentals of @lwc/state by building a loan application using atoms, computed values, and actions. We learned how to create a global singleton/factory store that provides reactive state manager across multiple components and pages. Today, we’ll delve into how the Platform State Manager can enhance this management further and share the data across the application.
But what if you’re building a Salesforce-integrated application that needs to:
- Retrieve record layouts dynamically from Salesforce
- Load field values based on those layouts
- Compose multiple data sources into a single state manager
- Inject state managers into component hierarchies without prop drilling
- Build reusable, configurable state managers
This is where the State Manager Pattern shines. In this advanced guide, we’ll explore how to build sophisticated state managers that integrate with Lightning Data Service, compose nested state managers, and leverage context-based dependency injection, taking full advantage of the Platform State Manager.
- State Manager Functions: Reusable factories that create configured state manager instances
- Nested State Managers: Composing multiple state managers together (state managers that use other state managers)
- Lightning Data Service Integration: Using built-in state managers like `stateManagerRecord` and `stateManagerLayout`
- Dynamic Configuration: State managers that adapt their behavior based on runtime configuration
The Problem: Loan Application Record Detail Panel
Let’s build a real-world Salesforce feature: a detail panel that displays record information. Get the full code at github repo
Requirements:
- Accept a
recordIdfor existing loan applications - Fetch the complete Loan_Application__c record with all fields
- Manage multi-step wizard flow (Personal → Employment → Financial → Review)
- Update record fields as user progresses through steps
- Calculate derived values (monthly payment, total assets, debt-to-income ratio)
- Handle loading states and validation
Why this is complex:
- Record management: Need to fetch and update Salesforce records
- Multi-step flow: Wizard state + record state coordination
- Derived calculations: Business logic that depends on record data
- Validation: Required fields and business rules
- State synchronization: UI state + record state must stay in sync
Traditional approach problems:
- Multiple
@wiredecorators with complex conditionals - Difficult to track loading states across multiple wires
- Hard to test and reuse
- Tight coupling between UI and data fetching logic
State Manager Pattern solution:
- Single state manager orchestrates record fetching and wizard flow
- Nested
stateManagerRecordhandles Salesforce record operations - Automatic reactivity between record data and UI
- Clean separation of concerns
- Highly testable and maintainable
Architecture: Loan Application State Manager
Here’s the structure of our loan application state manager:
┌─────────────────────────────────────────────────────────────┐
│ loanAppStateManagerRecord (Custom) │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Wizard State: │ │
│ │ • currentStep (1-4) │ │
│ │ │ │
│ │ Record Configuration: │ │
│ │ • recordId (Loan_Application__c ID) │ │
│ │ • fields (all loan application fields) │ │
│ │ │ │
│ │ Nested State Manager: │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ applicationRecord = stateManagerRecord(...) │ │ │
│ │ │ Manages Loan_Application__c record │ │ │
│ │ │ Handles all CRUD operations │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ Computed Values: │ │
│ │ • monthlyPayment (auto-calculated) │ │
│ │ • totalAssets (sum of all balances) │ │
│ │ • debtToIncomeRatio (financial ratio) │ │
│ │ • isApplicationComplete (validation) │ │
│ └────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
▲
│ (used by wizard)
┌───────┴────────┐
│ │
┌──────────────┐ ┌──────────────┐
│ Wizard │ │ Summary │
│ Component │ │ Component │
│ (Steps 1-4) │ │ (Dashboard) │
└──────────────┘ └──────────────┘
Both use same state manager instance

Step 1: Understanding Built-in State Managers
Salesforce provides built-in state managers for common operations:
1. stateManagerRecord (from lightning/stateManagerRecord)
import stateManagerRecord from 'lightning/stateManagerRecord';
// Create a state manager for a record
const recordManager = stateManagerRecord({
recordId: '001XXXXXXXXXXXX',
fields: ['Account.Name', 'Account.Industry', 'Account.Phone']
});
// Access the data
recordManager.value.data; // { id: '001...', fields: { Name: {...}, Industry: {...} } }
recordManager.value.error; // Error object if something went wrong
recordManager.value.status; // 'unconfigured' | 'loading' | 'loaded' | 'error'
Key features:
- Fetches record data using UI API
- Returns normalized record format
- Handles loading states and errors
- Can be reconfigured dynamically
2. stateManagerLayout (from lightning/stateManagerLayout)
import stateManagerLayout from 'lightning/stateManagerLayout';
// Create a state manager for a layout
const layoutManager = stateManagerLayout({
objectApiName: 'Account',
recordTypeId: '012XXXXXXXXXXXX',
layoutType: 'Compact' // or 'Full'
});
// Access the layout
layoutManager.value.data; // Layout definition with sections, fields, etc.
layoutManager.value.error; // Error object if something went wrong
layoutManager.value.status; // 'unconfigured' | 'loading' | 'loaded' | 'error'
Key features:
- Fetches layout metadata using UI API
- Returns layout structure (sections, fields, components)
- Supports different layout types
- Can be reconfigured for different record types
Step 2: Building the Loan Application State Manager
Now let’s compose the built-in stateManagerRecord with our custom wizard logic:
Key Concepts Explained:
1. State Manager Factory Function
export default defineState(({ atom, computed, setAtom }) => {
return (recordId, objectApiName) => {
// This inner function is called each time you create an instance
// Each instance has its own independent state
};
});
Unlike the singleton pattern from Blog 1, this is a factory function. Each call creates a new, independent state manager instance.
2. Nested State Managers
const initialRecord = smRecord(computed([config], () => {
return { recordId: '001...', fields: [...] };
}));
We pass a computed() value as the configuration for smRecord. This means:
- When
configchanges, the computed value recalculates - The new config is passed to
smRecord smRecordfetches new data automatically
3. Data Waterfall
initialRecord ─(recordTypeId)─> layout ─(fields)─> finalRecord ─> data
Each step depends on the previous one. computed() handles all the dependencies automatically.
4. Status Aggregation
const status = computed([initialRecord, layout, finalRecord], (...statuses) => {
// Combine statuses from all nested managers into single status
});
The overall status reflects the state of all nested managers.
loanAppStateManagerRecord.js
import { defineState } from '@lwc/state';
import smRecord from 'lightning/stateManagerRecord';
/**
* Loan Application State Manager using lightning/stateManagerRecord
*
* This state manager provides centralized state for the loan application wizard.
* It uses lightning/stateManagerRecord as a nested state manager for managing
* Salesforce Loan_Application__c records, and @lwc/state for wizard orchestration.
*
* Implements singleton pattern - calling with different recordIds returns the same instance
* and updates the recordId if needed.
*/
// Singleton instance storage
let stateInstance = null;
let currentRecordId = null;
export default defineState(({ atom, computed, setAtom }) => {
return (recordId) => {
// Return existing instance if already created
if (stateInstance && recordId === currentRecordId) {
return stateInstance;
}
// If recordId changed, reset the instance to create a new one
if (stateInstance && recordId !== currentRecordId && recordId) {
currentRecordId = recordId;
stateInstance = null; // Reset to create new instance with new recordId
}
// Return existing instance even if no recordId passed (allows child components to access)
if (stateInstance) {
return stateInstance;
}
// Create new instance only if none exists
currentRecordId = recordId;
// Step tracking atom
const currentStep = atom(1);
// Record configuration atom
const recordConfig = atom({
recordId: recordId || undefined,
fields: [
// Personal Info fields
'Loan_Application__c.First_Name__c',
'Loan_Application__c.Last_Name__c',
'Loan_Application__c.Email__c',
'Loan_Application__c.Phone__c',
'Loan_Application__c.SSN__c',
'Loan_Application__c.Date_of_Birth__c',
'Loan_Application__c.Street_Address__c',
'Loan_Application__c.City__c',
'Loan_Application__c.State__c',
'Loan_Application__c.ZIP_Code__c',
// Employment Info fields
'Loan_Application__c.Employer_Name__c',
'Loan_Application__c.Job_Title__c',
'Loan_Application__c.Employment_Type__c',
'Loan_Application__c.Monthly_Income__c',
'Loan_Application__c.Years_Employed__c',
'Loan_Application__c.Employer_Phone__c',
// Financial Info fields
'Loan_Application__c.Annual_Income__c',
'Loan_Application__c.Other_Income__c',
'Loan_Application__c.Monthly_Expenses__c',
'Loan_Application__c.Credit_Score__c',
'Loan_Application__c.Existing_Debts__c',
'Loan_Application__c.Checking_Balance__c',
'Loan_Application__c.Savings_Balance__c',
'Loan_Application__c.Investments_Balance__c',
'Loan_Application__c.Real_Estate_Value__c',
// Loan Details fields
'Loan_Application__c.Loan_Amount__c',
'Loan_Application__c.Loan_Purpose__c',
'Loan_Application__c.Loan_Term__c',
'Loan_Application__c.Interest_Rate__c',
// Status fields
'Loan_Application__c.Status__c',
'Loan_Application__c.CreatedDate',
'Loan_Application__c.LastModifiedDate'
]
});
// Nested state manager for the Loan Application record
// Pass the recordConfig atom directly - it will reactively update when recordId changes
const applicationRecord = smRecord(recordConfig);
// Computed value for monthly payment calculation
const monthlyPayment = computed([applicationRecord], (appRec) => {
if (!appRec || !appRec.data || !appRec.data.fields) return 0;
const fields = appRec.data.fields;
const principal = fields['Loan_Application__c.Loan_Amount__c']?.value || 0;
const annualRate = (fields['Loan_Application__c.Interest_Rate__c']?.value || 0) / 100;
const monthlyRate = annualRate / 12;
const termMonths = (fields['Loan_Application__c.Loan_Term__c']?.value || 30) * 12;
if (monthlyRate === 0) return principal / termMonths || 0;
return principal * (monthlyRate * Math.pow(1 + monthlyRate, termMonths)) /
(Math.pow(1 + monthlyRate, termMonths) - 1) || 0;
});
// Computed value for total assets
const totalAssets = computed([applicationRecord], (appRec) => {
if (!appRec || !appRec.data || !appRec.data.fields) return 0;
const fields = appRec.data.fields;
const checking = fields['Loan_Application__c.Checking_Balance__c']?.value || 0;
const savings = fields['Loan_Application__c.Savings_Balance__c']?.value || 0;
const investments = fields['Loan_Application__c.Investments_Balance__c']?.value || 0;
const realEstate = fields['Loan_Application__c.Real_Estate_Value__c']?.value || 0;
return checking + savings + investments + realEstate;
});
// Computed value for debt-to-income ratio
const debtToIncomeRatio = computed([applicationRecord], (appRec) => {
if (!appRec || !appRec.data || !appRec.data.fields) return 0;
const fields = appRec.data.fields;
const monthlyIncome = fields['Loan_Application__c.Monthly_Income__c']?.value || 0;
const monthlyDebts = (fields['Loan_Application__c.Existing_Debts__c']?.value || 0) / 12;
return monthlyIncome > 0 ? (monthlyDebts / monthlyIncome) * 100 : 0;
});
// Computed value for application completion status
const isApplicationComplete = computed([applicationRecord], (appRec) => {
if (!appRec || !appRec.data || !appRec.data.fields) return false;
const fields = appRec.data.fields;
return !!(
fields['Loan_Application__c.First_Name__c']?.value &&
fields['Loan_Application__c.Last_Name__c']?.value &&
fields['Loan_Application__c.Email__c']?.value &&
fields['Loan_Application__c.Phone__c']?.value &&
fields['Loan_Application__c.SSN__c']?.value &&
fields['Loan_Application__c.Date_of_Birth__c']?.value &&
fields['Loan_Application__c.Street_Address__c']?.value &&
fields['Loan_Application__c.City__c']?.value &&
fields['Loan_Application__c.State__c']?.value &&
fields['Loan_Application__c.ZIP_Code__c']?.value &&
fields['Loan_Application__c.Employer_Name__c']?.value &&
fields['Loan_Application__c.Job_Title__c']?.value &&
fields['Loan_Application__c.Monthly_Income__c']?.value > 0 &&
fields['Loan_Application__c.Annual_Income__c']?.value > 0 &&
fields['Loan_Application__c.Loan_Amount__c']?.value > 0 &&
fields['Loan_Application__c.Loan_Purpose__c']?.value
);
});
// Action to update current step
const updateCurrentStep = (step) => {
setAtom(currentStep, step);
};
// Action to update record fields
const updateRecordFields = (fieldData) => {
if (applicationRecord && applicationRecord.updateFields) {
applicationRecord.updateFields(fieldData);
}
};
// Store the instance
stateInstance = {
// State atoms/computed
currentStep,
applicationRecord,
recordConfig,
monthlyPayment,
totalAssets,
debtToIncomeRatio,
isApplicationComplete,
// Actions
updateCurrentStep,
updateRecordFields
};
// Return public API
return stateInstance;
};
});
Step 3: Using the State Manager in a Parent LWC
Now let’s use our state manager(loanAppStateManagerRecord) in a loan application wizard component:
loanWizardStateManager.js
import { LightningElement, api, wire } from 'lwc';
import loanAppStateManager from 'c/loanAppStateManagerRecord';
import { CurrentPageReference, NavigationMixin } from 'lightning/navigation';
/**
* Loan Application Wizard using lightning/stateManagerRecord
*
* This wizard manages a multi-step loan application with:
* - 4 distinct steps (Personal, Employment, Financial, Review)
* - Nested state management via lightning/stateManagerRecord
* - Reactive updates for Salesforce record data
* - Validation before step navigation
*/
export default class LoanWizardStateManager extends NavigationMixin(LightningElement) {
@api recordId;
objectApiName = 'Loan_Application__c';
state;
totalSteps = 4;
isInitialized = false;
connected = false;
currentPageReference;
// Wire the CurrentPageReference to track URL state changes
@wire(CurrentPageReference)
setCurrentPageReference(pageRef) {
this.currentPageReference = pageRef;
if (pageRef && pageRef.state) {
// Get recordId and objectApiName from URL parameters
const urlRecordId = pageRef.state.c__recordId;
const urlObjectApiName = pageRef.state.c__objectApiName;
console.log('URL Parameters:', {
recordId: urlRecordId,
objectApiName: urlObjectApiName
});
if (urlRecordId) {
this.recordId = urlRecordId;
}
if (urlObjectApiName) {
this.objectApiName = urlObjectApiName;
}
// Initialize state manager if we have recordId and haven't initialized yet
if (this.recordId && !this.isInitialized && this.connected) {
console.log('Initializing state manager with recordId:', this.recordId);
this.state = loanAppStateManager(this.recordId);
console.log('State initialized:', this.state);
this.isInitialized = true;
}
}
}
connectedCallback() {
this.connected = true;
console.log('Connected callback - recordId:', this.recordId);
// Initialize state manager if recordId is already available
if (this.recordId && !this.isInitialized) {
console.log('Initializing state manager in connectedCallback');
this.state = loanAppStateManager(this.recordId);
console.log('State initialized connectedCallback:', this.state);
this.isInitialized = true;
} else if (!this.state && !this.isInitialized) {
// Initialize with null if no recordId - this allows for new record creation
console.log('Initializing state manager with null recordId');
this.state = loanAppStateManager(null);
this.isInitialized = true;
}
}
get stateManagerRecord() {
return this.state?.value;
}
get currentStep() {
// Access currentStep directly as it's a primitive value in _value
return this.stateManagerRecord?.currentStep || 1;
}
get hasRecordData() {
// Access applicationRecord.data directly
return !!this.stateManagerRecord?.applicationRecord?.data;
}
get recordStatus() {
// Access applicationRecord.status directly
return this.stateManagerRecord?.applicationRecord?.status || 'loading';
}
handleNext() {
const currentStepValue = this.currentStep;
// Determine next action based on current step
const nextStep = currentStepValue + 1;
this.stateManagerRecord.updateCurrentStep(nextStep);
}
handlePrevious() {
const currentStepValue = this.currentStep;
if (currentStepValue > 1) {
const previousStep = currentStepValue - 1;
this.stateManagerRecord.updateCurrentStep(previousStep);
}
}
}Use Platform State Manager In Child Component
import { LightningElement } from 'lwc';
import loanAppStateManager from 'c/loanAppStateManagerRecord';
export default class PersonalInfoStepSM extends LightningElement {
state = loanAppStateManager(); // Access singleton instance
get stateManagerRecord() {
return this.state?.value; // Access the wrapped value
}
get applicationRecord() {
// Access the nested lightning/stateManagerRecord
return this.stateManagerRecord?.applicationRecord;
}
get fields() {
// Get Salesforce record fields
return this.applicationRecord?.data?.fields || {};
}
get firstName() {
// Bind directly to Salesforce field
return this.fields['First_Name__c']?.value || '';
}
// Handle input changes by updating Salesforce record
handleInputChange(event) {
const field = event.target.dataset.field;
const value = event.target.value;
// Update the Salesforce record through the nested state manager
this.applicationRecord.updateFields({
[`Loan_Application__c.${field}__c`]: value
});
}
}Pass Data to Cross-Page Components using Platform State Manager
loanApplicationSummarySM.js
import { LightningElement } from 'lwc';
import loanAppStateManager from 'c/loanAppStateManagerRecord';
export default class LoanApplicationSummarySM extends LightningElement {
state = loanAppStateManager(); // Same singleton instance
get stateManagerRecord() {
return this.state?.value;
}
get applicationRecord() {
return this.stateManagerRecord?.applicationRecord;
}
get fields() {
return this.applicationRecord?.data?.fields || {};
}
get applicantName() {
const firstName = this.fields['First_Name__c']?.value || '';
const lastName = this.fields['Last_Name__c']?.value || '';
return firstName && lastName ? `${firstName} ${lastName}` : 'Not provided';
}
}When to Use Platform State Manager Pattern vs Basic @lwc/state
Use Basic @lwc/state Blog 1 When:
- ✅ You need a simple global store
- ✅ All components share the same data
- ✅ Data doesn’t come from Salesforce APIs
- ✅ You’re building a single-page app feature
Use State Manager Pattern (Blog 2) When:
- ✅ You’re integrating with Salesforce APIs (records, layouts, etc.)
- ✅ You need multiple independent instances (different records)
- ✅ You have complex data dependencies (waterfalls)
- ✅ You want to compose multiple data sources
- ✅ You need context-based injection
- ✅ You’re building reusable components for different contexts
Additional Resources
- Official Documentation:
- GitHub Repository: state-management
- Previous Blog: Mastering LWC State Management with @lwc/state
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