Understand local & external JavaScript state management in Magento

Managing the client-side state in JavaScript is difficult, especially when dealing with a larger platform or architecture like Magento 2. The user interacts with the frontend of the website, and those interactions must be able to be persisted & saved over the duration of their session. When the 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 can 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 present a payment method. Let us not even mention gift certificates, promo codes, real-time shipping rates, ...the list continues, and goes on & on.

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:

define([
    'uiComponent',
    'ko'
], function(
    Component,
    ko
) {
    'use strict';

    return Component.extend({
        defaults: {
            simple: ko.observable('A default value.')
        },
        setSimple(value) {
            this.simple(value);
        },
        getSimple() {
            return this.simple();
        }
    });
});

Managing state like this with 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:

screencapture-magento-test-checkout-2021-08-22-11_25_30.png

When the Next button is clicked, the address information is pulled back out and displayed in multiple components on the Review & Payments screen:

screencapture-magento-test-checkout-2021-08-22-11_24_00.png

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 and links, 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.

Creating external state with models

The first step when implementing a more advanced state management process is pulling the state out into a separate file. When state isn't tied to a specific component, it acts as its own sovereign entity and facilitates multi-component use:

Local state.png

External state.png

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

Note how a simple JavaScript object is returned from this model, along with some getters & setters:

/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */
/**
 * @api
 */
define([
    'ko',
    'underscore',
    'domReady!'
], function (ko, _) {
    'use strict';

    var billingAddress = ko.observable(null),
        shippingAddress = ko.observable(null),
        shippingMethod = ko.observable(null),
        paymentMethod = ko.observable(null),
        ...

    return {
        totals: totals,
        shippingAddress: shippingAddress,
        shippingMethod: shippingMethod,
        billingAddress: billingAddress,
        paymentMethod: paymentMethod,
        guestEmail: null,
        ...
        
        /**
         *
         * @return {*}
         */
        getTotals: function () {
            return totals;
        },

        /**
         * @param {Object} data
         */
        setTotals: function (data) {
            data = proceedTotalsData(data);
            totals(data);
            this.setCollectedTotals('subtotal_with_discount', parseFloat(data['subtotal_with_discount']));
        },

        ...
    };
});

This file is the "single source of truth" for the management of this "quote" piece of state. All business logic related to quotes is contained within 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:

Model getters and setters.png

This requires an additional layer of abstraction, and is usually not needed. Returning all object properties within a model is simpler and is preferred, retaining the use of getters and setters for more complex business requirements.

Import model data 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:

/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */

define([
    ...
    'Magento_Checkout/js/model/quote',
    ...
],
function (
    ...
    quote,
    ...
) {
    'use strict';

    ...

    return Component.extend({
        ...
        currentBillingAddress: quote.billingAddress,
        ...

        canUseShippingAddress: ko.computed(function () {
            return !quote.isVirtual() && quote.shippingAddress() && quote.shippingAddress().canUseForBilling();
        }),

        ...
    });
});

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 to be introduced into the frontend user interface, at the risk of adding some additional complexity to your codebase.

Don't build external state models right away to avoid the risk of premature scaling. Start off with local state within components, which is always the simplest possible state manager, and works for most scenarios.

Letting your requirements or development experience drive your decisions is a solid approach to choosing a state manager. Once you outgrow local component state, the decision to move to an external state manager should be an obvious & easy choice.

Complete and Continue  
Extra lesson content locked
Enroll to access all lessons, source code & comments.
Enroll now to Unlock