All insights
Engineering

WordPress Actions and Filters — Keeping Hooks Clean

May 20, 2026 5 min read
WordPress Actions and Filters — Keeping Hooks Clean

WordPress hooks actions filters are the language the entire platform speaks — but without discipline, they become the thing that makes your codebase unmaintainable.

The Language WordPress Speaks

WordPress core, every plugin, and every theme communicate through a shared event system. An action fires — wp_head, save_post, wp_enqueue_scripts — and any code that has hooked into it runs. A filter passes a value through a chain of callbacks that can each modify it before it reaches its destination.

This is why WordPress is so extensible. You can change how the login form renders without touching core. You can add custom columns to the posts list without modifying WordPress’s admin code. You can intercept a WooCommerce checkout and add business logic without forking the plugin. The hook system is genuinely powerful — and in the hands of a developer who treats it casually, genuinely difficult to maintain.

How Hook Sprawl Happens

I’ve inherited WordPress codebases where functions.php was two thousand lines of hooks, many of them anonymous closures. No way to unhook them. No clear reason for the priority choices. Related functionality scattered across dozens of add_action calls with no grouping.

Two patterns cause most of this damage.

Anonymous functions as callbacks. add_action( 'init', function() { ... } ); is fast to write. It’s also impossible to remove. If a child theme or plugin ever needs to unhook that callback with remove_action(), there’s nothing to reference — anonymous functions are unhookable only if you hold a reference to the closure object. Most developers don’t. Once added, these hooks are permanent for the request lifecycle.

No grouping by concern. Enqueue scripts here. Register post types there. Modify the query somewhere else. Hooks scattered through a long functions.php mean that changing how your theme handles images requires searching the entire file to find every image-related hook, because they’re not together.

The Architecture That Scales

Two rules fix both problems and make hooks maintainable at any size.

Named functions only. Every hook callback is a named function — or a named method on a class. This makes the callback removable, testable, and greppable. When a junior developer asks “where does the custom query get modified?”, you can run grep -r "pre_get_posts" and find the answer in seconds. With anonymous functions, you can’t.

Group hooks by concern, not by type. Don’t group all your add_action calls together and all your add_filter calls separately. Group them by what they do. All post type registration together. All script enqueueing together. All WooCommerce modifications together. The unit of organization is the feature, not the hook type.

Code That Shows the Difference

Named function pattern versus anonymous — both work, only one is maintainable:

// Correct — named, removable, testable
function mytheme_enqueue_scripts() {
    wp_enqueue_style( 'mytheme-main', get_stylesheet_uri() );
}
add_action( 'wp_enqueue_scripts', 'mytheme_enqueue_scripts' );

// Also removable with: remove_action( 'wp_enqueue_scripts', 'mytheme_enqueue_scripts' );

// Wrong — anonymous, permanently attached, impossible to unhook
add_action( 'wp_enqueue_scripts', function() {
    wp_enqueue_style( 'mytheme-main', get_stylesheet_uri() );
} );

Class-based hooks for related functionality — the pattern I use in larger themes:

class MyTheme_Post_Types {

    public function register() {
        add_action( 'init', array( $this, 'register_cpts' ) );
        add_action( 'init', array( $this, 'register_taxonomies' ) );
    }

    public function register_cpts() {
        register_post_type( 'project', array(
            'public'    => true,
            'label'     => 'Projects',
            'supports'  => array( 'title', 'editor', 'thumbnail' ),
        ) );
    }

    public function register_taxonomies() {
        register_taxonomy( 'project_category', 'project', array(
            'public'       => true,
            'hierarchical' => true,
        ) );
    }
}

$post_types = new MyTheme_Post_Types();
$post_types->register();

Filter with a return value — the pattern many beginners get wrong:

// Filters must always return a value — missing return is a common bug
function mytheme_modify_excerpt_length( $length ) {
    return 20; // 20 words
}
add_filter( 'excerpt_length', 'mytheme_modify_excerpt_length' );

// With priority — run after WooCommerce's filter (priority 20)
function mytheme_woo_excerpt_length( $length ) {
    if ( is_singular( 'product' ) ) {
        return 30;
    }
    return $length;
}
add_filter( 'excerpt_length', 'mytheme_woo_excerpt_length', 25 );

At Scale: Hooks in a Real Theme

On the mosharafmanu.com theme, I organize hooks across separate files loaded from functions.php — one file per concern. inc/post-types.php handles CPT and taxonomy registration. inc/enqueue.php handles all script and style loading. inc/query-mods.php handles pre_get_posts modifications. Each file is short enough to read in under two minutes.

This structure means I can find any hook in under thirty seconds by looking in the right file. It also means a new developer can contribute to one area of the codebase without needing to understand the whole thing.

The Limits

The named-function pattern adds a small amount of verbosity compared to anonymous closures. For truly one-off, never-to-be-unhooked, non-tested callbacks — a quick admin notice, a one-time migration trigger — an anonymous function is reasonable. The rule isn’t a law; it’s a default that should have a reason when broken.

Engineering Takeaway

Hooks are WordPress’s greatest strength and, without discipline, its biggest maintenance liability. The developers who keep hook-heavy codebases readable aren’t using clever abstractions — they’re just consistently using named functions and grouping related hooks together. Two rules, applied everywhere. That’s the whole system.

Conversation

Join the discussion

Thoughts, corrections or war stories from your own builds — all welcome.

0 comments

No comments yet. Be the first.

Leave a Reply

Chat on WhatsApp