Do not depend on window.checkoutConfig

Within the Magento 2 checkout, JavaScript components have access to a variable window.checkoutConfig that contains useful stuff when dealing with the checkout logic (payment providers, checkout agreements, etcetera). However, using the window object in your JS library is a wrong thing to do. How to deal with this annoying variable?

What is window.checkoutConfig?

The variable window.checkoutConfig is a JavaScript object made available on the checkout and cart pages. It is initialized through a small statement within (for instance) the PHTML template onepage.phtml of the Magento_Checkout module:

window.checkoutConfig = <?php echo \Zend_Json::encode($block->getCheckoutConfig()); ?>;

And it contains a list of handy properties that might be used to enhance your own module in the checkout:

  • payment
  • checkoutAgreements
  • isGuestCheckoutAllowed
  • forgotPasswordUrl
  • reloadOnBillingAddress
  • customerData
  • ...

This blog is not meant as a way of documenting this variable and how to use it. Instead, it tries to point out that depending on a global variable is wrong and that you would want to get your hands on the same date using a different approach. It might be that you only 1 or 2 properties in the object, so why not get those specific properties separately?

Why not use window?

First of all, why is depending on the global window variable a bad thing? To cut things short, using global variables is something that is frowned upon with all modern programming languages and this also counts for JavaScript. Now that JavaScript has become more mature, we try to make sense of code by segmenting it, putting each code segment in its scope, where we control the dependencies from and to that scope carefully. Using globals makes that practice harder because a global is simply available in all scopes.

More practically, you could say that window is a fundament of the JavaScript language. But it isn't. If you run JavaScript in a Node environment, there is no window environment variable. I personally have started working with Mocha (run on top of NodeJS) to create unit tests for my JavaScript code and because I have no window in my Mocha scripts, I have to mock that object instead. No worries, we have tools for that. And with unit testing, you have to mock a lot more objects. But it does not take away the fact that relying on JavaScript globals is wrong.

Module pattern and RequireJS

I'm going to assume that you are familiar with how Magento 2 deals with JavaScript, using the module pattern to bind code to a scope and using RequireJS to load dependencies. Also, we are diving a bit in backend development parts, so knowledge of DI is assumed.

Where does checkoutConfig come from?

Within the onepage.phtml, we can see that the checkoutConfig variabe is simply a JSON object exported from the block method Magento\Checkout\Block\Onepage::getCheckoutConfig():

window.checkoutConfig = <?php echo \Zend_Json::encode($block->getCheckoutConfig()); ?>;

This method returns an array from yet another class:

return $this->configProvider->getConfig();

Extending config providers

And $this->configProvider is an instance of \Magento\Checkout\Model\CompositeConfigProvider, which collects a configuration array by looping through a list of Configuration Providers. You can add a new class that listing of providers by using a DI argument type:

<config ...>
    <type name="Magento\Checkout\Model\CompositeConfigProvider">
        <arguments>
            <argument name="configProviders" xsi:type="array">
                <item name="example_config_provider" xsi:type="object">
                    Yireo\Example\ConfigProvider\Dummy
                </item>
            </argument>
        </arguments>
    </type>
</config>

Within your Yireo\Example\ConfigProvider\Dummy class, you simply need to implement a method getConfig() that returns an array and you're done:

namespace Yireo\Example\ConfigProvider;
use Magento\Checkout\Model\ConfigProviderInterface;

class Dummy implements ConfigProviderInterface
{
    public function getConfig() : array
    {
        return [];
    }
}

Now you can return specific variables by simply adding name and value pairs to the return array.

But should you? I think you should not.

Injecting customerData in your JS component

In the window.checkoutConfig object, there is also a property customerData that you could use to access session-bound properties, even if you have enabled the Full Page Cache. If you zoom in on the onepage.phtml file, you can also see the following line:

window.customerData = window.checkoutConfig.customerData;

You can see that this object is a duplicate. But here is the important thing: Do NOT use either of these objects! Instead, if you write your own JS code, make sure to do so using the Magento 2 module pattern and using RequireJS to inject dependencies. Next, inject Magento_Customer/js/customer-data in your JS component and use something like var customer = customerData.get('customer'); to get information out of that object.

Your JS component would look a bit as follows:

define([
    'Magento_Customer/js/customer-data'
], function (customerData) {
    var customer = customerData.get('customer');
});

Do NOT use window.checkoutConfig.customerData

While there is a variable window.customerData and window.checkoutConfig available, there are cleaner ways to get the same information in your module, as shown above.

Likewise, if you want to add more information to this customerData object, you can. So extending customerData to add your own private data is possible, while injecting customerData as a clean RequireJS dependency in your core class. Do not use window.checkoutConfig.customerData.

Adding checkoutConfig to your JS component configuration

It might be that it is not just the checkoutConfig.customerData object that you are after, it might be that you need everything in the checkoutConfig parent object. Again, refrain yourself from using window, but instead, initialize your JS component in a similar way as the checkout initializes the checkout JS component.

I'm assuming you have a working JS component (based on KnockoutJS or not), a working Blocking class and a HTML section (with ID dummy) that you want to apply the JS component to:

<script type="text/x-magento-init">
{
    "#dummy": {
        "Magento_Ui/js/core/app": <?php echo $block->getJsLayout();?>
    }
}
</script>

And in your Block class, you can modify the getJsLayout() method to include the configuration as derived from an instance of \Magento\Checkout\Model\CompositeConfigProvider:

public function getJsLayout()
{
    $this->jsLayout['checkoutConfig'] = $this->configProvider->getConfig();
    return \Zend_Json::encode($this->jsLayout);
}

Please note that this example is far from finished. You still need to inject an instance of \Magento\Checkout\Model\CompositeConfigProvider in your PHP constructor. You need to make sure that jsLayout is extracted from the $data variable in the constructor. And the $data array needs to be initialized via XML layout code with the right parameters. That's outside the scope of this article.

I simply wanted to point out that you can inject yourself with the same properties of checkoutConfig without relying on the window global.

Writing to window.checkoutConfig?

I hope you are with me when I say that the procedures above allow for the usage of window.checkoutConfig to be phased out. Adding static configuration values to the getJsLayout() works nicely and still allows for the block output to be cached (while the configuration values are static). And if the configuration values are not static, but session-specific (customer, cart, quote) then the correct procedure of customerData allows you to solve that - with Full Page Caching still being functional.

But what happens if some JS component changes window.checkoutConfig on the fly, while in your own JS component you need that change? What if JS components start writing to window.checkoutConfig? I have not seen this in the Magento 2 core itself, but I have seen third party modules (payment gateways) doing this:

window.checkoutConfig.payment.xyz.response = response;

Answer: Do not use window. Use your JS component!

Again, the answer is that you should not depend on window. Instead, any module that modifies window.checkoutConfig should be rewritten to use KnockoutJS observables. Please note that while a third party module may have some reason to write temporary data (like a payment response) to the window.checkoutConfig object, the modified window.checkoutConfig is not picked up upon by the core. It is not persistent. The object disappears when the document is refreshed.

Instead of saving a response in window.checkoutConfig, it should be stored as a property in your module pattern return value (this.response). And if you want to make that response available to other JS components, turn it into a KnockoutJS observable (this.response = ko.observable('')). And if that response does not fit in the architecture of your JS component, create a new JS component (a response model?).

Why is it then in the core?

This article was meant to point out that using window.checkoutConfig is a bad thing to do. Why is it then in the Magento 2 core? There are numerous places where core JS components rely on the window global. And there is no real good reason for it. Alan Storm opened up a GitHub issue #7475 and core developer Anton Kril reacted by stating that it "violates our guidelines". So, there are plans to remove this window dependency. It simply shows that there is still some legacy code in Magento 2. You are welcome to submit PRs :)

Regardless of the state of the core, the main thing is that there is no good reason for you, as a third party developer, to reuse the window.checkoutConfig object in your code. Hopefully, this article gave you enough inspiration for alternatives.