Software, Development, and WordPress

Why Are These WordPress Hooks Firing Twice?

The hook system that’s built into WordPress is great and really powerful once you fully understand now only how the default actions and filters work, but how you can leverage them in your own themes and plugins to have others work for you.

But there’s a problem that comes with this: Other developers can often abuse them. Perhaps they will name a hook like one that already exists, or perhaps they’ll trigger a hook outside of the normal WordPress lifecycle.

When you’re working on building a plugin that’s adhering to the best practices of using a predefined hook and another plugin ends up breaking the usual flow of control, it can be extremely frustrating.

You – or at least I – can literally spend hours trying to isolate and trace down the source of the problem.

Frustrating, right?

Anyway, I’m not in the business of “calling other people out” or identifying problematic plugins on this site (though I don’t mind to discussing one on one), so this post is not about a plugin that’s doing things in a way that I don’t recommend.

Instead, it’s about finding ways to find a solution when you’re faced with a similar problem.

The WordPress Hooks Fire Twice (or More!)

So imagine this: You’re working on a plugin that’s going to add a new button to the TinyMCE editor in WordPress – maybe you’re doing to be adding one to the toolbar near the existing “kitchen sink” or maybe you’re going to be adding a button above the editor next to the “Add Media” button.

In both cases, WordPress hooks exist for doing just that:

Let’s say, for purposes of this example, that we’re going to be using mce_buttons to add a button next to the “Add Media” button just above the post editor. When running the plugin in isolation, all works fine.


Great – no big deal, right?

But then you end up deploying the code to Staging, and suddenly, your plugin looks like it’s there – because the button appears – but when you click it, maybe nothing happens. Or maybe multiple things happen all at once.

When this happens, there’s some different things that could be the problem, but one of the best things that you can do is begin to deactivate plugins and then start re-activating them one-by-one until you find the one that’s causing the problem.

In a case like the one that’s being outlined in this post, you’re likely going to find that there’s a plugin that’s triggering a hook – such as mce_buttons – outside of normal WordPress page lifecycle.

And while this may work great for that plugin, it’s also going to completely mess with anything else that’s using that hook because it’s going to fire every single event handler that’s registered with it meaning that your plugin is probably going to be triggered more than once and/or out of order, and then it’s not going to work correctly at all.

If you’re a developer, do not hijack WordPress’ default hooks. Name your own and then use `do_action` to fire them when needed.

Anyway, so how do you go about fixing this? Depending on your implementation and the offending plugin, you’re likely going to find that your solution is going to vary. But if you’re working with the hook that we’re discussing above, then this is how I’ve opted to solve it.

Solving The Problem

Note: I’m not sure if this is the correct way to handle, so if it’s not, please leave a comment outlining a better way to go about doing this.

The plugin that I’m building works in the following way:

  1. Display a new button in the editor above the normal toolbar next to the “Add Media” button
  2. When the button is clicked, display a modal (that accepts input from the user)
  3. Take the input from the modal and initiate an Ajax request
  4. Handle the response from the Ajax request and then enter a result into the post editor

The problem is that if another plugin is hooked into the mce_buttons action, then the plugin will be called more than once and the second (or third, fourth, etc.) instance of the dialog is what the user is going to see.

And the problem is that all of the event handlers defined by JavaScript are registered with the first instance which is hidden behind the other instances.

Still with me?

So to make sure that only a single instance of the dialog is displayed, I introduce a counter variable into the JavaScript, and I increment it when the dialog is displayed.

Next, if the event is called again – by an offending plugin – but the counter has already been incremented, then my plugin doesn’t execute any more.

See the following code:

Again, this works will within the context of this plugin, though I don’t know if it’s a correct way of doing so (and I’m interested in your feedback).

The biggest takeaway from this, in my opinion, is that we need to be careful what we name our custom actions, and we need to make sure that we’re not carelessly triggering other actions that are already built into WordPress as it can introduce a significant level of side effects.

This is frustrating for other developers and users, and it makes your plugin or project look careless – it monopolizes the WordPress experience.

A Note About Development and Staging

One thing that I didn’t mention in the actual article itself, but that’s helpful nonetheless, is that your Development environment should mirror Staging and vice versa.

This means that if there are things on Staging that are not on your local machine, then you need to make sure to pull them down and set them up so that you have a 1:1 configuration. This will allow you to catch problems before they hit staging and save some development time.

Sure, it’s time-consuming on the front-end of the project, but it saves a lot of work towards the middle of the project when you’re cycling through the various test cases. So in addition to everything that’s outlined above, make sure that you have this setup locally and on Staging, as well.


  1. Nick Ciske

    Core has a function called did_action to track how many times an action has been called:

    Same basic idea as your JS counter, so it would seem you’re on the right track.

    • Tom

      Ah, this is exactly what I was looking to find last week but didn’t have success (clearly), so I may go back and refactor the code to work on the server-side instead :).

      Thanks Nick!

    • Damian logghe

      Nice catch! I had the same problem with the save_post_{custom_post} hook last week and I ended using a static variable to check this. Of course only happens on certain sites and not everytime :)

  2. Thomas Scholz

    Another example for that is a multisite. Other code can call switch_to_blog() and then run the same actions again. Always check for is_multisite() && ms_is_switched() before you run your callback logic..

    • Tom

      Always check for is_multisite() && ms_is_switched() before you run your callback logic..

      This is one of those things that I always remember to do after-the-fact. I don’t do a lot of work with multisite so it’s not something that’s on the forefront of my mind.

      Probably a bad excuse, but it’s the truth :).

  3. Damien Carbery

    Query Monitor has a Hooks section – would that list the functions attached to the hook you are using?

    • Tom

      It should. Most of the time, I use that plugin and it identifies problematic areas exactly as I’d suspect.

      When I was debugging this particular issue, it showed the hooks that were being used, but none of them were highlighted in red so it was a bit more tedious that normal to go down and isolate the culprit (as there were a number of plugins being used).

  4. Andrew Fielden

    This is something that also vexes me and I think that it is a major weakness in coding with WordPress.

    A solution that I think is a good idea is that adopted by Advanced Custom Fields where there is a sort of namespace methodology so they start ‘acf/’ something and that something can be extended e.g. ‘acf/fields/taxonomy/query’

    If there was a standard that said that all custom hooks and filters present in the WP plugin directory should use their own unique quasi namespace then this might help. Heck you could even extend it to non .org plugins.

  5. Alec Kinnear

    Great advice, Tom. We were running into some of these conflicts with hooks last week. Very timely advice. It’s important as we build more complex sites that all developers take great care in naming hooks.

    It would be great to see a straight best practices check list (including the checks for multisite: we’re doing more and more multisite as publishers build mini-networks) which could be reviewed by every programmer before releasing.

    • Tom

      Thanks Alec – and I agree, though I can only imagine how long that checklist would get ;).

Leave a Reply

© 2020 Tom McFarlin

Theme by Anders NorenUp ↑