Nested State Manager Example

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);