← Back to blog

Magento 2 RequireJS Mixins: Extend Core JS Without Overrides

Rodrigo Biazzi

When a Magento 2 storefront needs a JavaScript change in core or third party code, the tempting fix is to copy the file into the theme and edit it. It works until the next upgrade, when the original changes and your copy silently keeps the old code. RequireJS mixins solve this the same way plugins solve it in PHP: you wrap or extend the original module and change only the part you care about.

This article shows how mixins work in Magento 2.4.8, the three patterns you need for the three kinds of JavaScript modules in Luma, and how to check that a mixin is actually loaded.

How mixins work

Magento adds a small RequireJS plugin, mixins, that sits between a module and whoever requires it. When a module has mixins configured, RequireJS loads the original, then passes its return value to each mixin in turn. Whatever the last mixin returns is what the rest of the code receives.

A mixin is therefore just a function that takes the original module export and returns a modified one. What you return depends on what the original module returns, and that is where most mistakes happen.

Declaring a mixin

Mixins are declared in requirejs-config.js, in a module (view/frontend/requirejs-config.js) or in a theme (web/requirejs-config.js, or under a module folder inside the theme).

var config = {
    config: {
        mixins: {
            'Magento_Checkout/js/view/shipping': {
                'Vendor_Checkout/js/view/shipping-mixin': true
            },
            'Magento_Catalog/js/catalog-add-to-cart': {
                'Vendor_Catalog/js/catalog-add-to-cart-mixin': true
            },
            'Magento_Checkout/js/action/place-order': {
                'Vendor_Checkout/js/action/place-order-mixin': true
            }
        }
    }
};

The key is the module path of the original, exactly as other code requires it. The value is a map of your mixin paths to true. Setting a mixin to false in a later loaded config disables it, which is a clean way to turn off a mixin from a third party module without editing vendor code.

All requirejs-config.js files are merged into one file per theme and locale during static content deploy. In production mode, nothing changes until you deploy static content again.

Pattern 1: UI components

A UI component module returns a constructor built with Component.extend(). Your mixin receives that constructor and returns an extended one.

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

    return function (Shipping) {
        return Shipping.extend({
            validateShippingInformation: function () {
                var isValid = this._super();

                if (isValid && !this.isDeliveryNoteValid()) {
                    return false;
                }

                return isValid;
            },

            isDeliveryNoteValid: function () {
                return true;
            }
        });
    };
});

this._super() calls the original method, including any changes from mixins that ran before yours. Always call it unless you have a reason to replace the method completely, and if you do, write that reason down in the commit message. Replacing a method breaks every other mixin that wraps it.

For more on how these components are built, see the UI components and Knockout basics.

Pattern 2: jQuery widgets

Many Luma modules are jQuery UI widgets declared with $.widget(). The module returns the widget constructor. To extend it, you redeclare the widget with the original as the base and return the new one.

define([
    'jquery'
], function ($) {
    'use strict';

    return function (widget) {
        $.widget('mage.catalogAddToCart', widget, {
            ajaxSubmit: function (form) {
                this.element.trigger('vendor:beforeAddToCart');

                return this._super(form);
            }
        });

        return $.mage.catalogAddToCart;
    };
});

The widget name must match the original name exactly. If you register it under a new name, the code that calls catalogAddToCart keeps using the original and your mixin does nothing. Open the original file and read the name in its $.widget call before writing the mixin.

Pattern 3: modules that return a function

Actions in the checkout, such as place-order or set-shipping-information, return a plain function. You cannot extend a function, so you wrap it with mage/utils/wrapper.

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

    return function (placeOrderAction) {
        return wrapper.wrap(placeOrderAction, function (originalAction, paymentData, messageContainer) {
            paymentData.extension_attributes = paymentData.extension_attributes || {};
            paymentData.extension_attributes.delivery_note = quote.deliveryNote || '';

            return originalAction(paymentData, messageContainer);
        });
    };
});

The wrapper passes the original function as the first argument, followed by the real arguments. Return the result of the original call, because the caller often expects a promise. Forgetting the return is a classic way to break the place order button without any console error.

Sending an extra value with the payment data also needs a server side part, usually an extension attribute and a plugin. The checkout custom field article covers that end to end.

Modules that return an object

Some modules return a plain object with methods, such as several checkout models. In that case, change the method on the object and return the same object:

define([
    'mage/utils/wrapper'
], function (wrapper) {
    'use strict';

    return function (target) {
        target.someMethod = wrapper.wrap(target.someMethod, function (original) {
            var args = Array.prototype.slice.call(arguments, 1);

            return original.apply(this, args);
        });

        return target;
    };
});

Because the object is shared, the change applies to every module that uses it, which is usually what you want.

Mixin vs map vs copying the file

  • Mixin: you change one or two methods and keep everything else. This should be your default.
  • map in requirejs-config.js: you replace the whole module with your own file for every caller. Use it when you need to rewrite most of the module. You take over maintenance of all of it.
  • Theme override of the same path: same effect as map for that theme, and the same maintenance cost.

This is the same reasoning as choosing between a plugin and a preference in PHP. The plugins vs observers vs preferences article explains that side in detail.

Keeping mixins maintainable

Mixins are cheap to write, so stores tend to collect a lot of them. A few habits keep that under control.

  • One mixin file per original module. If you need three changes to the shipping component, put them in one mixin. Three separate files make the call chain harder to follow when something breaks.
  • Name the file after the original. shipping-mixin.js next to a path that mirrors the original makes it obvious what it extends without opening the config.
  • Keep business logic out of the mixin. Put the logic in its own module and call it from the mixin. The mixin then stays a thin adapter that is easy to adjust when the original changes.
  • Review mixins on every upgrade. Compare the methods you wrap against the new version of the original. When the original renames or removes a method, your override is no longer called by the core code, and nothing warns you about it.

Checking that a mixin is loaded

When a mixin seems to do nothing, check these in order:

  1. Open requirejs-config.js for your theme and locale in pub/static/frontend/Vendor/theme/en_US/ and search for your mixin path. If it is missing, the config file is not being picked up or static content is stale.
  2. In the browser console, run require.s.contexts._.config.config.mixins and look for the original module key.
  3. Check the Network tab to see whether your mixin file loads at all.
  4. Confirm the key matches the path the page actually requires. A module required as Magento_Checkout/js/view/shipping will not pick up a mixin declared for a different alias of the same file.
  5. For widgets, confirm the widget name matches the original.

There is no sort order for mixins. When several mixins target the same module, the order follows the order in which the config files are merged, which follows module sequence. If your mixin must run after another one, add that module to the sequence in your module.xml.

FAQ

Where do I put requirejs-config.js for a mixin?

In a module, put it in view/frontend/requirejs-config.js. In a theme, put it in web/requirejs-config.js or in the module folder inside the theme. Deploy static content afterwards in production mode.

Can I use a mixin in the admin?

Yes. Declare it in view/adminhtml/requirejs-config.js. The same three patterns apply to admin JavaScript modules.

How do I disable a mixin from another module?

Declare the same original module and mixin path in your own requirejs-config.js with the value false, and make sure your module loads after the other one through sequence in module.xml.

Do mixins work on Hyvä?

No. Hyvä does not use RequireJS on the storefront, so Luma mixins do not run there. Hyvä compatibility modules replace that JavaScript with Alpine.js code.

I am available for freelance Magento and Adobe Commerce work, including checkout JavaScript and Luma customisations. If you need help with this, get in touch.

Related articles