Data Guidelines
Lightning Data Service
State Management Compared with Alternatives
Share a State Manager Across Components
Nested State Managers
State Management Examples
Call APIs from Apex
Work with Errors
Develop Secure Code
This more complex state manager illustrates the use of nested state managers. In this case, the nested state managers are provided by the Salesforce platform. (Documentation for these built-in state managers is forthcoming.)
This example is excerpted from the platform-state-managers example in State Management Examples. The code is the same, but the comments are a bit more detailed.
1import { defineState } from "@lwc/state";
2import smRecord from "lightning/stateManagerRecord";
3import smLayout from "lightning/stateManagerLayout";
4
5/**
6 * Extracts the fields referenced by a layout.
7 *
8 * @param {*} layout layout definition
9 * @returns fields referenced by the layout, as a string[]
10 */
11function extractFields(layout) {
12 if (!layout) {
13 return;
14 }
15
16 const fields = [];
17
18 for (const section of layout.sections) {
19 for (const row of section.layoutRows) {
20 for (const item of row.layoutItems) {
21 for (const component of item.layoutComponents) {
22 if (component.componentType === "Field") {
23 fields.push(`${layout.objectApiName}.${component.apiName}`);
24 }
25 }
26 }
27 }
28 }
29
30 return fields;
31}
32
33// Define the state manager
34export default defineState(({ atom, computed, setAtom }, recordId, objectApiName) => {
35 // This atom captures the current configuration of the state manager instance.
36 // We wrap it in an atom to make it easier for other data to react to changes
37 // in the config. It is NOT exposed as one of the properties of this state
38 // manager.
39 const config = atom({ recordId, objectApiName });
40
41 // Actions to set or change the config
42 const setRecordId = (recordId) =>
43 setAtom(config, { recordId, objectApiName: config.value.objectApiName });
44 const setObjectApiName = (objectApiName) =>
45 setAtom(config, { recordId: config.value.recordId, objectApiName });
46
47 // The following constructs implement a data waterfall that corresponds roughly
48 // to what happens in a layout-driven detail panel like you see on record home.
49 // (The real detail panel is FAR more complex; this is a VERY simplified version
50 // of the data logic from that component.)
51 //
52 // 1. The `recordId` and `objectApiName` supplied as config to this state manager are
53 // used to retrieve a minimal copy of the record so that we can find its
54 // `recordTypeId`.
55 // 2. The `objectApiName` and `recordTypeId` from (1) are used to retrieve the layout
56 // that should be used for the record.
57 // 3. The `recordId`, `objectApiName`, and fields referenced in the layout are used
58 // to retrieve a complete copy of the record that can be displayed.
59
60 // `initialRecord` is a nested state manager that is used to retrieve a minimal copy
61 // of the record (step 1 above). `computed()` is used here so that the configuration
62 // for `smRecord` will automatically be updated anytime the config changes.
63 const initialRecord = smRecord(
64 computed([config], ({ recordId, objectApiName } = {}) => {
65 // If the state manager does not yet have a `recordId` or `objectApiName` then
66 // pass an empty config to `smRecord`; the empty config will cause `smRecord` to wait.
67 if (!recordId || !objectApiName) {
68 return {};
69 }
70
71 // Once we have `recordId` and `objectApiName`, tell `smRecord` to get the record.
72 // The `Id` field is requested here because we have to specify at least one field.
73 // The data we really want (the `recordTypeId`) is included by default.
74 return {
75 recordId,
76 fields: [`${objectApiName}.Id`],
77 };
78 }),
79 );
80
81 // `layout` is another nested state manager that we use to retrieve the layout. Its inputs
82 // are derived from the output of `initialRecord`, so `computed()` is used to update
83 // the config every time `initialRecord` changes.
84 const layout = smLayout(
85 computed([initialRecord], ({ data: recordData }) => {
86 // If `initialRecord` has not retrieved the record yet then return an empty config
87 // so `smLayout` will wait.
88 if (!recordData) {
89 return {};
90 }
91
92 // Once `initialRecord` has data available, use its `apiName` and `recordTypeId` to ask
93 // for the Compact View layout.
94 return {
95 objectApiName: recordData.apiName,
96 recordTypeId: recordData.recordTypeId,
97 layoutType: "Compact",
98 mode: "View",
99 };
100 }),
101 );
102
103 // `finalRecord` is used to retrieve the full set of field values that the layout needs.
104 // `computed()` is used here so that the config for this second `smRecord` will be re-evaluated
105 // whenever `initialRecord` or `layout` changes.
106 const finalRecord = smRecord(
107 computed([initialRecord, layout], ({ data: recordData }, { data: layoutData }) => {
108 // Tell `smRecord` to wait if we don't have the necessary information yet.
109 if (!recordData || !layoutData) {
110 return {};
111 }
112
113 // Once we have all the information, ask for all the layout's fields for the
114 // record.
115 return {
116 recordId: recordData.id,
117 fields: extractFields(layoutData),
118 };
119 }),
120 );
121
122 // `data` is a just a more consumable form of the field values in `finalRecord`.
123 // As such, we want to update it every time `finalRecord` changes.
124 const data = computed([finalRecord], ({ data: recordData }) => {
125 if (!recordData) {
126 return;
127 }
128
129 const fieldValues = {};
130 for (const [field, value] of Object.entries(recordData.fields)) {
131 fieldValues[field] = value.displayValue || value.value;
132 }
133
134 return fieldValues;
135 });
136
137 // `error` is an aggregation of errors from `initialRecord`, `layout`, and `finalRecord`.
138 const error = computed(
139 [initialRecord, layout, finalRecord],
140 ({ error: initialRecordError }, { error: layoutError }, { error: finalRecordError }) =>
141 initialRecordError || layoutError || finalRecordError,
142 );
143
144 // `status` lets consumers of this state manager understand what's going on.
145 const status = computed(
146 [initialRecord, layout, finalRecord],
147 (
148 { status: initialRecordStatus },
149 { status: layoutStatus },
150 { status: finalRecordStatus },
151 ) => {
152 // We configure `initialRecord` as soon as this state manager is configured, so we
153 // can assume its "unconfigured" status means that this state manager is also
154 // unconfigured.
155 if (initialRecordStatus === "unconfigured") {
156 return "unconfigured";
157 }
158 // any errors => error
159 else if (
160 initialRecordStatus === "error" ||
161 layoutStatus === "error" ||
162 finalRecordStatus === "error"
163 ) {
164 return "error";
165 }
166 // everything loaded => loaded
167 else if (
168 initialRecordStatus === "loaded" &&
169 layoutStatus === "loaded" &&
170 finalRecordStatus === "loaded"
171 ) {
172 return "loaded";
173 }
174 // other status combinations mean something is still loading
175 else {
176 return "loading";
177 }
178 },
179 );
180
181 // This is the external shape that consumers of this state manager will see.
182 return {
183 // Data properties
184 data,
185 error,
186 status,
187
188 // Actions
189 setObjectApiName,
190 setRecordId,
191 };
192 };
193);