SponsoredHosinger Special Offer

WP+ WordPress Plugin Development

Build WordPress plugins from scratch: actions and filters, shortcodes, settings pages, custom post types, meta boxes, AJAX, REST endpoints, custom tables, cron and blocks.

AJAX in Plugins

WordPress Plugin Development Lesson 18 of 27 ~4 min read

Overview

AJAX lets a page talk to WordPress without reloading: your JavaScript sends a request to admin-ajax.php with an action name, WordPress runs the PHP callback registered for that action, and the callback replies with JSON.

The flow

  1. The visitor clicks "Favourite".
  2. JavaScript posts action=gdc_books_favourite, the book ID and a nonce to admin-ajax.php.
  3. WordPress fires wp_ajax_gdc_books_favourite (logged in) or wp_ajax_nopriv_gdc_books_favourite (logged out).
  4. Your callback verifies the nonce and capability, does the work, and replies with wp_send_json_success().
  5. JavaScript updates the button.

The PHP handler

<?php
function gdc_books_ajax_favourite() {
    // Dies with -1 and a 403 if the nonce is invalid.
    check_ajax_referer( 'gdc_books_favourite', 'nonce' );

    $user_id = get_current_user_id();
    $book_id = isset( $_POST['book_id'] ) ? absint( $_POST['book_id'] ) : 0;

    if ( ! $book_id || 'gdc_book' !== get_post_type( $book_id ) ) {
        wp_send_json_error( array( 'message' => __( 'Unknown book.', 'gdc-books' ) ), 400 );
    }

    $favourites = (array) get_user_meta( $user_id, 'gdc_books_favourites', true );
    $is_saved   = in_array( $book_id, $favourites, true );

    if ( $is_saved ) {
        $favourites = array_values( array_diff( $favourites, array( $book_id ) ) );
    } else {
        $favourites[] = $book_id;
    }

    update_user_meta( $user_id, 'gdc_books_favourites', array_map( 'absint', $favourites ) );

    wp_send_json_success(
        array(
            'saved' => ! $is_saved,
            'count' => count( $favourites ),
        )
    );
}
// Logged-in users only. Favourites belong to an account, so there is no nopriv hook.
add_action( 'wp_ajax_gdc_books_favourite', 'gdc_books_ajax_favourite' );

wp_send_json_success() and wp_send_json_error() set the JSON content type, print {"success":true,"data":{...}} and stop execution — you never need a wp_die() after them.

The JavaScript

// assets/js/favourites.js
document.addEventListener( 'click', async ( event ) => {
    const button = event.target.closest( '[data-gdc-favourite]' );
    if ( ! button ) {
        return;
    }

    button.disabled = true;

    const body = new URLSearchParams( {
        action: 'gdc_books_favourite',
        nonce: window.gdcBooks.nonce,
        book_id: button.dataset.gdcFavourite,
    } );

    try {
        const response = await fetch( window.gdcBooks.ajaxUrl, {
            method: 'POST',
            credentials: 'same-origin',
            body,
        } );
        const json = await response.json();

        if ( json.success ) {
            button.setAttribute( 'aria-pressed', String( json.data.saved ) );
            button.textContent = json.data.saved ? window.gdcBooks.i18n.saved : window.gdcBooks.i18n.removed;
        }
    } finally {
        button.disabled = false;
    }
} );

And the button, printed by your template or shortcode:

<?php if ( is_user_logged_in() ) : ?>
    <button type="button" class="gdc-favourite" aria-pressed="false"
            data-gdc-favourite="<?php echo esc_attr( get_the_ID() ); ?>">
        <?php esc_html_e( 'Favourite', 'gdc-books' ); ?>
    </button>
<?php endif; ?>

wp_ajax_nopriv: logged-out visitors

Register wp_ajax_nopriv_{action} only when logged-out visitors genuinely need the endpoint — a "load more books" button, for example. Anything there is reachable by anyone, so it must be read-only or strictly limited, and should never trust a user ID sent from the browser.

Tip: For new code, the REST API (next lesson) is usually the better choice: proper HTTP status codes, URL routes, schema validation and built-in authentication. admin-ajax.php remains common and perfectly valid for small, plugin-specific interactions.

Practice

  • Build the favourite button and handler, and watch the request in the browser's Network tab.
  • Change the nonce in DevTools before clicking and confirm the request fails with 403.
  • Add a "load more books" endpoint with both wp_ajax_ and wp_ajax_nopriv_ hooks.
Create a free account to save which lessons you've finished. Save my progress