Implement a State Manager

After you understand the "outer" syntax of using defineState to create a state manager, it's time to learn more about the internal implementation of a state manager.

Use the defineState function to define a state manager. When you call defineState, you provide a callback function. This callback receives an object with specific instances of state manager functions (primitives) that you use to define your state manager's behavior. defineState returns a function that, when invoked, gives you an instance of your state manager.

Properties represent the internal, consistent state of your state manager. Use the primitives atom and computed to create reactive properties that connect with the rest of LWC's reactivity system.

Represents a single, reactive piece of data. Changes to an atom trigger updates in components that use it, and re-computation of dependent derived values.

Provide an argument myValue to set the initial value.

The result of calling the atom() function is a reactive "atomic" value, which is essentially a wrapper around a piece of data that LWC can use to make components and other state managers reactive to changes in that data. Change the value of an atom only by using the setAtom primitive function.

Save the result of calling atom in a const variable, and return any atoms your state manager makes public in the state manager definition function's return statement.

You can wrap any JavaScript value, but it's recommended to stick to JSON-serializable values or undefined for best compatibility.

Represents data that's derived from one or more atoms or other computed values. A computed property automatically recalculates after its dependencies change.

Provide as arguments an array of values [independentValues] upon which the computed value is dependent, and an anonymous computationFunction that determines the derived value. The anonymous function receives as parameters the new or current values of the values upon which it depends, which it can use to calculate the new dependent value.

The formal explanation might read like gibberish, so here's something specific, taken from the succeeding example. doubleCount is twice count. This line is how that's implemented.

doubleCount is a computed property. The [count] array is the first argument to computed, which means that doubleCount depends on count.

Whenever count changes, the anonymous function that's the second argument to computed is called. It takes the same number of parameters as the size of the array in the first parameter, in this case, one: countValue. countValue is the value of count after the change. The result of the anonymous function is countValue * 2, and here it's implicitly returned.

This is about the simplest computed value possible. For a non-trivial example, including default values and an explicit return, see Nested State Manager Example.

Save the result of calling computed() in a const variable, and return any computeds that your state manager makes public in the state manager definition function's return statement.

Actions are functions defined by your state manager that contain logic to modify your state atom properties using the setAtom primitive. Actions are a part of the internal implementation of your state manager, and ensure that your state manager always maintains a consistent state. Public actions allow components and other state managers to interact with your state manager.

Define actions as anonymous functions, which can accept any parameters you want. Actions can return a value of any type, or no value. Actions can be synchronous or asynchronous.

Here's a specific action, setCount, taken from the full example later in this topic.

Here, the action is setCount, and it accepts one parameter, newValue, which is (unsurprisingly) the new value to set count to.

Save the action function in a const property. Return any actions that your state manager makes public in the state manager definition function's return statement.

Actions are a part of a state manager's implementation, and possibly part of the state manager's public API, but are otherwise "just functions". This offers flexibility, but just because you can do something doesn't mean you should.

For example, it's a best practice to always return a consistent shape for the state manager instance (same property names and value types, and the same actions). While possible, the following code would be an anti-pattern:

This anti-pattern would cause some instances of the state manager to include an additional action. Consumers of the state manager can't rely on the extra action always being available.

The callback function that you provide to defineState returns an object with references to the values and actions that your state manager makes available to its consumers. Your components access this public API via the value element of the state manager instance.

In this example, the function returns two values (one atom, one computed) and two actions:

A consumer component or template accesses the values using the .value element of the counter state manager instance:

Actions are accessed in a similar way from the component. To use them from the template, create an action handler in your component to connect the two together.

See a later section for the complete example.

State managers must not rely on DOM-specific APIs or data — elements, events, and so on. They're intended to help you manage data in your application. Interactions with the DOM belong in components.

This example is an expanded version of the counter state manager used in Syntax Introduction: A Simple Counter. While still simple, it illustrates all three state manager implementation functions, and how to interact with the state manager from a component's user interface.

This example puts the counterManager state manager in a supplemental JavaScript file, separate from the simpleCounter component. While easy to use, it's not a best practice for a state manager you intend to share across many components — which is much of the point of state managers. The better way to define a state manager is in an API module component. See Share a State Manager Across Components for an example.