Brown Rock Formation, de Ian Stauffer

The WordPress development stack has changed a lot in recent years. Since the arrival of Gutenberg, JavaScript’s role in our favorite CMS is more important than ever. In this blog we have already talked at length about the advantages that this entails for developers (to name a few examples, we’ve talked about extensions for Gutenberg, advice on TypeScript and React, development tools, plugin examples, and more) but every story has its dark side… and that’s what we’ll talk about here today.

In today’s post, I’m going to share with you the 3 main problems you, a WordPress plugin developer, might face in the future. And the funny part is, each one of them has a different culprit: either WordPress itself, other developers, or yourself. So here we go: the most common JavaScript headaches you might encounter and what you can do to avoid/fix them.

#1 WPO Plugins that will Break your Plugin and your Site

Let’s start with the problem that’s been the source of plenty of tickets here at Nelio: WPO plugins.

I am sure you’ve read plenty of articles and blogposts that outline the importance of having a light website that loads fast. I mean, we also have written on several occasions about it! Among the tips they usually give, you’ll find things like switching to a better hosting provider, using cache plugins and CDNs, keeping your server and WordPress up to date, or (and here comes the first problem) install a WPO plugin. Some examples of the latter include:

These plugins promise to speed up your website through a series of sensible optimizations that, in general, “any WordPress site can benefit from.” These optimizations include:

  • Dequeuing unnecessary scripts on the frontend, such as emojis or Dashicons
  • Caching pages and database queries
  • Reducing the amount of information included in the header
  • Combining and minifying JavaScript scripts and CSS styles
  • Minifying HTML
  • Removing the version query arg from the URLs of your static assets
  • Deferring JavaScript scripts and/or loading them asynchronously
  • etc

As I was saying, these types of optimizations might be, in general, beneficial. But in our experience, all JS optimizations on a WordPress website tend to result in more problems, thus rendering their supposed improvements useless. Real examples I’ve seen over the years are:

  • Combining scripts. The fewer scripts your browser has to request, the better. That’s why it makes sense to combine all scripts into one. However, this can be problematic. In general, if a JavaScript script crashes, its execution ends and the error is reported in your browser’s console. But only the execution of that script is stopped; your other scripts will run normally. But if you combine them all… well, as soon as one script fails, the other scripts (including maybe yours) won’t run, and your users will think it’s your plugin the one that’s not working as expected.
  • Minifying scripts. I’ve seen some minification processes that, believe it or not, broke regular expressions and resulted in JS scripts that had syntax errors. Sure, it’s been a while since the last time I encountered this one but… :-/
  • Query arguments. When you enqueue a script in WordPress, you can do so with its version number (which, by the way, was probably auto-generated by @wordpress/scripts). Version numbers are extremely helpful: if you update your plugin and your script changed, this new version number will guarantee that all visitors see a different URL and, therefore, their browsers will request the new version. Unfortunately, if a WPO plugin removes the query string, your visitors may not realize the script has changed and they’ll be using a cached copy of said script… which may or may not result in unintended consequences. Damn!

A complete disaster, isn’t it? But wait until you hear the next one:

Deferring scripts

At Nelio we have implemented an A/B Testing plugin with which to track your visitors and discover which design and content gets the most conversions. As you can imagine, our tracking script looks similar to this:

window.NelioABTesting = window.NelioABTesting || {};
window.NelioABTesting.init = ( settings ) => {
  // Add event listeners to track visitor events...
  console.log( settings );
};

That is, it exposes an init function we have to call in order to let the script know about the tests that are currently running. In order to call this method, we enqueue an inline script in PHP as follows:

function nab_enqueue_tracking_script() {
  wp_enqueue_script( 'nab-tracking', ... );
  wp_add_inline_script(
    'nab-tracking',
    sprintf(
      'NelioABTesting.init( %s );',
      wp_json_encode( nab_get_tracking_settings() )
    )
  );
}
add_action( 'wp_enqueue_scripts', 'nab_enqueue_tracking_script' );

which results in the following HTML tags:

<head>
  ...
  <script
    type="text/javascript"
    id="nab-tracking-js"
    src="https://.../dist/tracking.js"
  ></script>
  <script
    type="text/javascript"
    id="nab-tracking-js-after"
  >
    NelioABTesting.init( {"experiments":[...],...} );
  </script>
  ...
</head>
<body>
...

But what happens when your WPO plugin adds the defer attribute to our script?

<head>
  ...
  <script
    defer <!-- This delays its loading... -->
    type="text/javascript"
    id="nab-tracking-js"
    src="https://.../dist/tracking.js"
  ></script>
  <script
    type="text/javascript"
    id="nab-tracking-js-after"
  >
    NelioABTesting.init( {"experiments":[...],...} );
  </script>
  ...
</head>
<body>
...

Well, the script is now deferred… which means the previous snippet is equivalent to this one:

<head>
  ...
  <script
    type="text/javascript"
    id="nab-tracking-js-after"
  >
    NelioABTesting.init( {"experiments":[...],...} );
  </script>
  ...
</head>
<body>
  ...
  <script
    type="text/javascript"
    id="nab-tracking-js"
    src="https://.../dist/tracking.js"
  ></script>
</body>
</html>

and, as a result, nab-tracking-js is no longer loaded when it’s supposed to and, therefore, the inline script that comes after it and relies on it will simply fail: nab-tracking-js-after uses NelioABTesting.init which, thanks to the defer directive, is not available yet. Awful!

Nelio A/B Testing

Native Tests for WordPress

Use your WordPress page editor to create variants and run powerful tests with just a few clicks. No coding skills required.

Solution

The most effective solution is clear: tell your users to disable script optimization and call it a day. After all, dependency management in JavaScript is, in general, extremely complicated (especially if we use defer and async directives), and WordPress is no exception. Just take a look at this 12 year old discussion on the topic!

But if that is not feasible (and I know it’s not), I recommend you do the same thing we did: get rid of the init method and reverse the responsibilities of you regular and inline scripts. That is, add an inline script before the regular script and use it to define a global variable with the require settings:

function nab_enqueue_tracking_script() {
  wp_enqueue_script( 'nab-tracking', ... );
  wp_add_inline_script(
    'nab-tracking',
    sprintf(
      'NelioABTestingSettings = %s;',
      wp_json_encode( nab_get_tracking_settings() )
    ),
    'before'
  );
}
add_action( 'wp_enqueue_scripts', 'nab_enqueue_tracking_script' );

so that the resulting HTML looks like this:

<head>
  ...
  <script
    type="text/javascript"
    id="nab-tracking-js-before"
  >
    NelioABTestingSettings = {"experiments":[...],...};
  </script>
  <script
    type="text/javascript"
    id="nab-tracking-js"
    src="https://.../dist/tracking.js"
  ></script>
  ...
</head>
<body>
...

and so it doesn’t matter if the execution of the external script is delayed or not—it will always appear after the inline script, thus satisfying the dependency between them.

Finally, if you want to make sure that no one is going to modify your settings, declare the variable as const and freeze its value with Object.freeze :

...
sprintf(
  'const NelioABTestingSettings = Object.freeze( %s );',
  wp_json_encode( nab_get_tracking_settings() )
),
...

which is supported by all modern browsers.

#2 Dependencies in WordPress that may or may not work…

Dependency management can also be problematic in WordPress, especially when talking about WordPress’ built-in scripts. Let me explain.

Imagine, for example, that we are creating a small extension for Gutenberg, as we explained here. Our plugin’s source code will probably have some import statements like these:

import { RichTextToolbarButton } from '@wordpress/block-editor';
import { __ } from '@wordpress/i18n';
import { registerFormatType } from '@wordpress/rich-text';
// ...

When this JS source code is transpiled, Webpack (or the tool you use) will package all the dependencies and your own source code into a single JS file. This is the file you will later enqueue from WordPress so that everything works as you expect.

If you used @wordpress/scripts to create such a file, some of the dependencies will not be included in the output file, because the built-in process assumes the packages will be available in the global scope. This means the previous imports will be transpiled into something similar to this:

const { RichTextToolbarButton } = window.wp.blockEditor;
const { __ } = window.wp.i18n;
const { registerFormatType } = window.wp.richText;
// ...

In order to make sure you don’t miss any of your script’s dependencies, @wordpress/scripts will not only transpile your JS code, but it’ll also generate a PHP file with its WordPress dependencies:

<?php
return array(
  'dependencies' => array('wp-block-editor','wp-i18n','wp-rich-text'),
  'version'      => 'a12850ccaf6588b1e10968124fa4aba3',
);

Pretty neat, huh? So, what’s the issue? Well, these WordPress packages are under continuous development and they change quite often, adding new features and improvements. Therefore, if you develop your plugin using the latest version of WordPress, you might end up inadvertently using functions or features that are available in that latest version (and therefore everything works as it should) but not in “older” WordPress versions…

How can you tell?

Solution

My advice here is very simple: develop your plugins using the latest WordPress version, but test your releases on older versions. In particular, I’d suggest you test your plugin with, at least, the minimum WordPress version your plugin is supposed to support. A minimum version you’ll find in your plugin’s readme.txt:

 === Nelio Content ===
...
Requires PHP: 7.0
Requires at least: 5.4
Tested up to: 5.9
...

Switching from one WordPress version to another is as easy as running the following WP CLI command:

wp core update --version=5.4 --force

#3 Arrow functions are trickier than you think

Finally, let me share one of the latest problems I’ve encountered just a few days ago and which drove me crazy. In a nutshell, we had a JavaScript file similar to this one:

import domReady from '@wordpress/dom-ready';
domReady( () =>
  [ ...document.querySelectorAll( '.nelio-forms-form' ) ]
    .forEach( initForm )
);
// Helpers
// -------
const initForm = ( form ) => { ... }
// ...

which initializes your Nelio Forms on the front-end. The script is quite straightforward, isn’t it? It defines an anonymous function that’s called when the DOM is ready. This function uses a helper (arrow) function called initForm. Well, as it turns out, such a simple example can crash! But only under some specific circumstances (i.e. if the script was “optimized” by a WPO plugin using the defer attribute).

Here’s how JS executes the previous script:

  1. The anonymous function inside domReady is defined
  2. domReady runs
  3. If the DOM isn’t ready yet (and it usually isn’t when a script is loaded), domReady doesn’t run the callback function. Instead, it simply keeps track of it so that it can call it later
  4. JavaScript continues parsing the file and loads the initForm function
  5. Once the DOM is ready, the callback function is finally called

Now, what if, by the time we reach the third step, the DOM is ready and, therefore, domReady calls the anonymous function directly? Well, in that case, the script will trigger an undefined error, because initForm is still undefined.

In fact, the most curious thing about all this is that these two solutions being equivalent:

domReady( aux );
const aux = () => {};
domReady( () => aux() );
const aux = () => {}

the JavaScript linter will only throw an error on the first one, but not the latest. 😐

Solution

There are two possible solutions: either you define the helper function using the function keyword and forget about the arrow function, or you move the domReady statement at the end, after all the helper functions have been defined:

domReady( aux );
function aux() {
  // ...
}
const aux = () => {
  // ...
};
domReady( aux );

If you’re wondering why the first solution works if it’s apparently equivalent to the original one we had, it’s all about how JavaScript hoisting works. In short, in JavaScript you can use a function (defined with function) before its definition, but you can’t do the same with variables and constants (and therefore arrow functions).

In Summary

There is a huge number of things that can go wrong in JavaScript. Luckily, they all have a solution, especially if we pay attention to what we do. I hope you learned something new today and I trust that thanks to the mistakes and mistakes that I have made in the past, you will be able to avoid suffering them in your own flesh in the future.

Featured image by Ian Stauffer on Unsplash.

Leave a Reply

Your email address will not be published. Required fields are marked *