Managing the client-side state in JavaScript is difficult, especially when dealing with a larger platform or architecture such as Magento 2. As a user navigates through the frontend of the website, those interactions must be able to be persisted & saved over the duration of their session. When a user interacts with the interface, backend services are called in the form of REST API endpoints, which call PHP scripts that interact with database scripts, which then persist backend state.
One of the most notorious areas of managing client-side state in Magento is the checkout. A user may be a guest or logged in. When they log in, the previous state of their shopping cart is merged with their current session. A user can have any number of shipping or billing addresses, their order can be split out into multiple shipments, and they must select a payment method in order to place their order.
Let us not even mention gift certificates, promo codes, real-time shipping rates, ...the list continues, and goes on & on.
Local state
State can be as simple as a single property value on a component. For example, the simple component property below keeps track of a single piece of state:
State completed defined & used within a single component is called "local state". It is highly preferred, as is easy to understand & reason about. But as business logic gets more complex, this simple local state manager just doesn't scale along with it.
An example of the need for a more complex state is using street addresses during checkout. We can see this even with a simple checkout using a Check or Money Order. When the checkout starts, a shipping address is entered:
When the Next button is clicked, the address information is pulled back out and displayed in multiple components on the Review & Payments screen:
Once multiple components need access to a single piece of state, a more advanced state management process is needed.
You can indeed use
imports
,exports
andlinks
, but this practice is a good one to avoid. These linking utilities make state extremely difficult to debug, because it's hard to tell which files change what state.
External state
The first step when implementing a more advanced state management process is pulling state out into separate files. When state isn't tied to a specific component, it acts as its own sovereign entity, and promotes use by many components:
Magento references external state managers as "models". An example of a model is the quote model located at Magento_Checkout::view/frontend/web/js/model/quote.js
Notice how a simple JavaScript object is returned from this model, along with some getters & setters:
This file is the "single source of truth" for the management of this "quote" piece of state. All business logic related to a quote is contained in this model.
Controlling state access
Any property added to a model's exported object will be publicly accessible by other files, and could therefore be changed by others.
If more control over what files can access or modify state is needed, you can architect your models, so the only way to get or modify a piece of state is with a getter or setter:
This requires an additional layer of abstraction, and is usually not needed. Returning all object properties within a model is simpler and preferred, so you can retain the use of getters and setters for more complex business requirements.
Import models to access external state
In order to use this external state, just import the model into your file or component.
For example, Magento_Checkout::view/frontend/web/js/view/billing-address.js
pulls in the quote model right within the dependencies with define
:
One can then access all exported object properties, including but not limited to observable functions, computed functions, subscriptions, and so on.
Conclusion
This external state management process allows for complex business logic into your frontend user interface, at the risk of adding some additional complexity to your codebase.
It's advised to not create external state models right away to avoid the risk of premature scaling. Start off with local component state, which is always the simplest possible way to manage state and works best for most scenarios.
Letting your requirements or development experience drive decisions is a solid approach to choosing a state manager. When you outgrow local state, the decision to move to an external state manager should be an easy & obvious choice.
Is Knockout.js getting the best of you? If so, you may want to check out these resources:
- Become so efficient to Defeat Knockout.js in Magento 2 (700+ students)
- Grow your Magento expertise with all courses & lessons (700+ students)
- Learn visually with blocks of code & inline comments (3,000+ students)