Debugging Checkout

Working out why a checkout component renders nothing, using the console, the registry and window.checkoutConfig.

Syntaxrequire(['uiRegistry'], function (registry) { registry.get('<component.name>'); })

Checkout fails quietly. PHP errors land in var/log, but a component that is missing, misnamed or unrendered produces no log line at all, so the browser console is the primary tool here.

Start with the two globals

window.checkoutConfig holds everything PHP sent. If a key your config provider adds is absent, the problem is in di.xml or in caches, not in JavaScript: run bin/magento cache:flush, and bin/magento setup:di:compile in production mode. The registry holds every live component by its dotted name; registry.get(name) returning undefined means the component was never created.

A component that renders nothing

Work through three causes in order. The component is not in the merged tree, so check with a layout processor that logs the array. It is in the tree but its displayArea matches no getRegion() call in the parent template, so it exists in the registry and has nowhere to draw. Or its template did not load, which shows as a 404 in the Network tab for a path under pub/static.

The error that names no file

Knockout reports a failed binding as Unable to process binding followed by the binding text and the underlying error, and names no module, because the template is a string by then. The binding text identifies it: search the module's view/frontend/web/template directory for that expression. The usual cause is reading a property as a function, or the reverse.

What trips people up

Checkout is never full-page cached, so stale output means static assets. In developer mode delete pub/static/frontend and var/view_preprocessed, then reload.

Example

Example · javascript
// Browser console, on /checkout

// 1. What did PHP send?
Object.keys(window.checkoutConfig);
window.checkoutConfig.deliveryNote;      // undefined => di.xml or cache problem

// 2. Which Knockout version is loaded, and does the component exist?
require(['ko', 'uiRegistry'], function (ko, registry) {
    console.log(ko.version);

    var name = 'checkout.steps.billing-step.payment.afterMethods.delivery-note-hint',
        component = registry.get(name);

    if (!component) {
        console.log('not created: check the merged jsLayout');

        return;
    }

    console.log(component.getTemplate());           // template it will fetch
    console.log(component.containers[0].name);       // the parent it renders into
});

// 3. Which component owns the element you clicked?
//    Select a node in the Elements panel first, then:
require(['ko'], function (ko) {
    console.log(ko.dataFor($0).name);
    console.log(ko.contextFor($0).$data.getTemplate());
});

When to use it

  • Find out why a component added to checkout never appears.
  • Trace which module changed the payment step's component tree.
  • Decide whether a stale checkout is caches, static content or your code.
  • Locate the template behind a Knockout binding error that names no file.

More examples

Clearing the right things

Most checkout bugs that survive a correct fix are stale assets. Match the command to the file you changed: markup and JavaScript live in pub/static, interception and generated getters live in generated/code, and layout XML lives in the cache. Check the mode first, because removing pub/static on a production site takes the storefront down until the deploy finishes.

Example · bash
# Developer mode: a template or JS edit that will not show up
bin/magento cache:flush
rm -rf pub/static/frontend var/view_preprocessed var/cache

# Production mode: the same edit, deployed
bin/magento setup:static-content:deploy -f en_US
bin/magento cache:flush

# A di.xml, extension_attributes.xml or plugin change
bin/magento setup:di:compile
bin/magento cache:flush

# Confirm which mode you are in before doing any of the above
bin/magento deploy:mode:show
# Current application mode: developer. (Note: Environment variables may override this value.)

Logging the merged component tree

Register this last in the layoutProcessors array so it sees what every other processor produced, run bin/magento cache:flush, load checkout, then read var/log/debug.log. Serializing one branch rather than the whole array keeps the line readable. This answers the first question in any missing-component hunt: is the node in the tree at all, and did another module change it? Remove the class once you have the answer.

Example · php
<?php
// app/code/Vendor/DeliveryNote/Block/Checkout/Debug.php
declare(strict_types=1);

namespace Vendor\DeliveryNote\Block\Checkout;

use Magento\Checkout\Block\Checkout\LayoutProcessorInterface;
use Magento\Framework\Serialize\SerializerInterface;
use Psr\Log\LoggerInterface;

class Debug implements LayoutProcessorInterface
{
    public function __construct(
        private readonly LoggerInterface $logger,
        private readonly SerializerInterface $serializer
    ) {
    }

    public function process($jsLayout)
    {
        $node = $jsLayout['components']['checkout']['children']['steps']['children']
            ['billing-step']['children']['payment']['children']['afterMethods'] ?? null;

        $this->logger->debug('afterMethods: ' . $this->serializer->serialize($node));

        return $jsLayout;
    }
}

Watching a component that arrives late

Components are created as their RequireJS modules resolve, so a get() on page load often returns undefined for something that appears a second later. async(name) returns a function that takes your callback and defers it; get(name, callback) is the shorter form of the same wait. Subscribing to an observable is how to see what a component does during a shopper's interaction without adding console.log lines to a vendor file.

Example · javascript
// Browser console, on /checkout
require(['uiRegistry'], function (registry) {
    var name = 'checkout.steps.shipping-step.shippingAddress';

    // Runs as soon as the component registers, however late that is.
    registry.async(name)(function (shipping) {
        console.log('registered', shipping.name);

        shipping.isFormPopUpVisible.subscribe(function (visible) {
            console.log('address popup visible:', visible);
        });
    });

    // Shorthand for the same wait, when you only need the instance once:
    registry.get(name, function (shipping) {
        console.log('ready', shipping.getTemplate());
    });
});

Discussion

  • Be the first to comment on this lesson.