require.js, jQuery and race conditions

Hi folks

I am working on one old legacy ASP.NET WebForms project. It had a lot of inline JavaScript rendered from server-side. Personally I think it is a very bad practice. I think the modern world tends to use static JavaScript files and unobtrusive markup.

I don’t see an easy way to refactor that quickly so I’m improving the code gradually.

I started to use require.js with my new scripts and migrate legacy scripts slowly.

Let me show you the problem I faced.

Due to the fact that I have to use both AMD scripts and legacy-abusing-window-object scripts I had to load jQuery twice.

On my master page I have

<asp:ScriptManager runat="server">
    <Scripts>
        <asp:ScriptReference Path="~/components/requirejs/require.js" />
        <asp:ScriptReference Path="~/_scripts/require.config.js" />
        <asp:ScriptReference Path="~/_scripts/some-new-amd-module.js" />
        <asp:ScriptReference Path="~/components/jquery/dist/jquery.js" />
        <asp:ScriptReference Path="~/_scripts/some-legacy-script.js" />
    </Scripts>
</asp:ScriptManager>

I load jquery.js explicitly because some-legacy-script.js depends on it.

some-new-amd-module.js looks like

(function script() {
    "use strict";

    define(["jquery", "some-jquery-plugin"], function module($) {
        $(function domReady() {
            $(".my-selector").somePlugin(); // The problem appears here
        });
    });
})();

The plugin *some-jquery-plugin.js* is not AMD plugin, it is just doing something like

$.fn.somePlugin = function() {
    // some logic
}

And the problem is the following. Sometimes the call for somePlugin() fails (the line is commented above). It fails saying that somePlugin() does not exist.

It is not the permanent problem, it is pretty random which made me think that it is caused by race condition. And after some debugging when the issue occurred, I found that

> $ === window.$
false

require.js loads modules asynchronously so don’t know when exactly this will happen and in what order.

Our problem is the following: sometimes the load order happens to be

  1. jQuery loaded by require.js. It set window.$ variable and stored it internally within require.js cache
  2. jQuery loaded by a normal script tag. It overrode window.$ variable
  3. some-jquery-plugin.js is loaded. It set window.$.fn.somePlugin function
  4. some-new-amd-module.js is loaded. It took $ variable from require.js cache and tried to use $.fn.somePlugin function

and BANG!

So the problem was identified and how are we going to fix it?

One obvious solution is to get rid of local $ variable and this will fallback to the window.$

define(["jquery", "some-jquery-plugin"], function module() {

I don’t like this solution. I would prefer to write truly modular code.

I tried some approaches and none of them worked reliably. Finally I ended up with the one I would like to share with you.

require.config.js

(function script() {
    "use strict";

    require.config({
        map: {
            '*': { jquery: "jquery-fix-conflict" },
            "jquery-fix-conflict": { jquery: "jquery" }
        },
        shim: {
            "some-jquery-plugin": ["jquery"]
        }
    });
})();

and

jquery-fix-conflict.js

(function script() {
    "use strict";

    define(["jquery"], function module($) {
        setTimeout(function resetModule() {
            require.undef("jquery-fix-conflict");
        }, 0);
        window.jQuery = window.$ = $;
        return $;
    });
})();

This looks like a dirty hack and I wish to get rid of it but probably it will live until I rewrite everything to use AMD scripts.

Stay tuned

P.S. You may noticed that I don’t use anonymous functions and provide some name for all functions even for IIFE ones. That’s my recent habit I’ve got after watching https://frontendmasters.com/courses/advanced-javascript/ The author Kyle Simpson made his point of readable call stacks.

P.S 2: All the require.js samples I’ve seen suggest to start your module straight with define but my habits force me to start with IIFE and “use strict”

Advertisements

About mnaoumov

Senior .NET Developer in Readify
This entry was posted in Uncategorized and tagged , , . Bookmark the permalink.

One Response to require.js, jQuery and race conditions

  1. jonathanconway says:

    Very clever fix.

    My only comment is that CommonJS seems to be favoured more and more in the open-source world, over RequireJS, as I believe it allows for more flexibility with bundling tools (e.g. WebPack). But no doubt you’re constrained in that aspect as well, from how you describe the situation.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s