Understand local & external JavaScript state management in Magento

Managing application state is always difficult, and knowing when to use local or external state can help guide a proper architectural decision for your Magento store.

Mark Shust

Mark Shust

Last Updated 7 min read

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:

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

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.

Local state

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:

Shipping address at checkout

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

Review & Payments at checkout

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.

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:

External state

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:

/**
 * 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 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:

Model getters and setters

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:

/**
 * 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 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:

  1. Become so efficient to Defeat Knockout.js in Magento 2 (700+ students)
  2. Grow your Magento expertise with all courses & lessons (700+ students)
  3. Learn visually with blocks of code & inline comments (3,000+ students)