A Guide to WordPress Hooks

Actions, Filters, and the Request Lifecycle.

Part 1: The "What" and "Why" — Understanding Hooks

At its core, a hook is a specific point in the WordPress code where developers can insert their own custom functions ("callbacks") to either perform an action or modify data.

Think of the WordPress core as a factory assembly line. Hooks are the designated ports along that line where you can plug in your own tools to add a new feature or change the product as it goes by. This system is brilliant because it allows you to customize everything without ever altering the core factory machinery, ensuring your changes are safe during software updates.

Action Hooks: To Do Something

An action is like an event notification. It announces that something has just happened (e.g., "The post has been saved!"). You use add_action() to run a function at that specific moment.

Purpose
To do something at a specific moment.
Registration Function
add_action()
Must Return Data?
No. An action hook's callback function performs a task and doesn't need to return a value.

Filter Hooks: To Change Something

A filter is like a quality control checkpoint. It hands you a piece of data (e.g., a post title) and says, "Check this before it goes on display." You use add_filter() to modify that data and hand it back.

Purpose
To change (modify) data before it's used.
Registration Function
add_filter()
Must Return Data?
Yes. A filter hook's callback function MUST return the modified (or original) value.

Part 2: The "When" — Hooks in the WordPress Lifecycle

To use hooks effectively, you must know when they fire. Here is the chronological flow of the most common hooks during a WordPress request.

Phase 1: The Bootstrap (WordPress is Waking Up) ☀️

These hooks fire as WordPress loads its core, plugins, and theme—before any page content is determined.

plugins_loaded (action)
When: After all active plugins are loaded.
What it's for: A primary setup hook for plugins. It's the first reliable point to safely use functions from other plugins.
after_setup_theme (action)
When: Immediately after the theme’s functions.php is loaded.
What it's for: The main setup hook for themes. Used to register theme features like navigation menus (register_nav_menus), post thumbnails (add_theme_support('post-thumbnails')), and image sizes (add_image_size).
init (action)
When: After WordPress is fully loaded and the user is authenticated.
What it's for: The most widely used "workhorse" hook. It’s the perfect time to register custom post types, custom taxonomies, and handle URL routing.

Phase 2A: The Front-End Request (Building the Page) 🖼️

These hooks fire when a visitor requests a public-facing page.

pre_get_posts (action)
When: Just before the main database query is run.
What it's for: Incredibly powerful for altering the main page query. Use it to exclude categories from the blog page, change the post order, or modify archive pages without writing a new database query.
template_redirect (action)
When: After the database query is complete but before the theme template file is loaded.
What it's for: The standard hook for performing redirects. For example, redirecting non-logged-in users from a members-only page.
wp_head (action)
When: In the <head> section of the HTML document.
What it's for: The standard place to add scripts, styles (wp_enqueue_scripts/wp_enqueue_style), and meta tags.
the_content (filter)
When: As post content is being prepared for display.
What it's for: The classic filter for modifying post content. Perfect for adding social sharing buttons, disclaimers, or related posts.
wp_footer (action)
When: Just before the closing </body> tag.
What it's for: Used to add JavaScript files or tracking codes that should be loaded at the end of the page.

Phase 2B: The Admin Request (The Dashboard) ⚙️

These hooks are specific to the WordPress admin area.

admin_init (action)
When: At the start of every admin page load.
What it's for: The init hook for the backend. Used to register settings (register_setting), handle admin form submissions, and enqueue assets.
admin_menu (action)
When: As the admin sidebar menu is being built.
What it's for: The correct hook to add new menu pages (add_menu_page) to the dashboard.
save_post (action)
When: Fired whenever a post, page, or custom post type is created or updated.
What it's for: Essential for saving custom field data from a metabox.

Other Common Hooks for Plugin Development

Beyond the main request lifecycle, several other hooks are essential tools for building robust plugins and themes.

wp_enqueue_scripts (action)
When: Fires on the front-end as WordPress gathers scripts and styles to output.
What it's for: The only correct way to add CSS stylesheets and JavaScript files to the front-end of a site. It manages dependencies and prevents duplicate loading.
admin_enqueue_scripts (action)
When: Fires on admin pages as WordPress gathers scripts and styles.
What it's for: The correct way to add CSS and JavaScript to the WordPress admin area. The callback function receives the page's hook suffix (e.g., 'post.php'), allowing you to load assets only on the specific admin pages where they are needed.
add_meta_boxes (action)
When: Fires after the main admin post editor has been set up.
What it's for: The primary hook for adding custom metaboxes (panels with custom fields) to the post, page, or custom post type editing screens using the add_meta_box() function.

Part 3: The "How" — Practical Usage & Best Practices

Knowing the theory is one thing; applying it correctly is another.

The Syntax: add_action() & add_filter()

The basic syntax is the same for both:

add_action( 'hook_name', 'your_callback_function', [priority], [accepted_args] );
  • 'hook_name': The name of the hook you want to attach to (e.g., 'init').
  • 'your_callback_function': The name of your function that will run.
  • [priority] (optional): An integer (default is 10). A lower number runs earlier.
  • [accepted_args] (optional): The number of arguments your function accepts (default is 1).

Creating Your Own Custom Hooks

You can make your own plugins and themes extensible by adding your own hooks. This allows other developers to interact with your code without modifying it.

Create a custom action
do_action( 'myplugin_before_widget', $data );
Create a custom filter
$title = apply_filters( 'myplugin_widget_title', $title );

Essential Best Practices

  • Prefix Everything (Namespacing): This is the most important rule. To avoid conflicts with other plugins, prefix all your function and custom hook names with a unique identifier.
    Bad: function save_data()
    Good: function whm_save_data()
  • Use Descriptive Names: Make your code readable. function whm_add_author_bio_to_content is much better than function whm_add_stuff.
  • Control Hook Execution:
    • Remove Hooks: Use remove_action() or remove_filter() to detach a function from a hook (you must match the priority and arguments).
    • Use Conditionals: Wrap your add_action or your callback logic in WordPress conditional tags (e.g., if ( is_single() ) { ... }) to ensure code only runs where it's needed.

The Complete Picture: A Masterclass Code Example

This single plugin file demonstrates all the concepts working together.

<?php
/**
 * Plugin Name: WP Hooks Masterclass Demo
 * Description: A comprehensive demo showing action, filter, and custom hooks in practice.
 * Author:      Your Name
 */

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

// ======== 1. ADDING ACTIONS (with Priority) ========
// This action registers a custom post type. It runs early (priority 5) during the 'init' hook.
add_action( 'init', 'whm_register_cpt', 5 );
function whm_register_cpt() {
    register_post_type( 'masterclass_example', [
        'public' => true,
        'label'  => 'Masterclass Examples',
    ]);
}

// This action adds a message to the site's footer. It runs later (priority 20).
add_action( 'wp_footer', 'whm_add_footer_credit', 20 );
function whm_add_footer_credit() {
    echo '<p style="text-align:center;">Powered by the Hooks Masterclass Plugin.</p>';
}

// ======== 2. ADDING FILTERS (with Arguments) ========
// This filter prepends text to post titles, but only on the front-end.
// It accepts 2 arguments ($title, $post_id) passed by the 'the_title' hook.
add_filter( 'the_title', 'whm_filter_post_title', 10, 2 );
function whm_filter_post_title( $title, $post_id ) {
    // Conditional logic: Don't change titles in the admin dashboard.
    if ( is_admin() ) {
        return $title;
    }
    return '[Example] ' . $title;
}

// ======== 3. CREATING & USING CUSTOM HOOKS ========
// This function demonstrates creating our own hooks for extensibility.
function whm_process_some_data( $data ) {
    // A custom action that fires before processing.
    do_action( 'whm_before_data_processed', $data );

    // A custom filter that allows the data to be modified.
    $data = apply_filters( 'whm_filter_data_before_save', $data );

    // (Imagine data saving logic is here)
    error_log( 'Processed data: ' . print_r( $data, true ) );

    // A custom action that fires after processing.
    do_action( 'whm_after_data_processed', $data );
}

// Now, we hook into our OWN custom hooks.
add_action( 'whm_before_data_processed', 'whm_log_raw_data' );
function whm_log_raw_data( $data ) {
    error_log( 'Starting to process: ' . print_r( $data, true ) );
}

add_filter( 'whm_filter_data_before_save', 'whm_add_timestamp_to_data' );
function whm_add_timestamp_to_data( $data ) {
    $data['processed_timestamp'] = current_time( 'mysql' );
    return $data; // A filter MUST return the modified value.
}

// ======== 4. REMOVING HOOKS CONDITIONALLY ========
// Let's remove the footer credit on the homepage.
add_action( 'template_redirect', 'whm_remove_footer_credit_on_homepage' );
function whm_remove_footer_credit_on_homepage() {
    if ( is_front_page() ) {
        // We must match the function name and priority used in add_action.
        remove_action( 'wp_footer', 'whm_add_footer_credit', 20 );
    }
}

// Let's trigger our custom function to see the custom hooks in action.
whm_process_some_data( [ 'user_id' => 1, 'value' => 'test' ] );