Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

AJAX requests

A "Like" button, loading more posts, submitting a form without a reload — all of these are requests from JavaScript to the server. WordPress has an old, proven mechanism for them: the admin-ajax.php file and the wp_ajax_ hooks.

A handler

Every request carries an action parameter. WordPress uses it to call a hook of the same name:

// functions.php
 
add_action('wp_ajax_mytheme_like', 'mytheme_handle_like');
add_action('wp_ajax_nopriv_mytheme_like', 'mytheme_handle_like');
 
function mytheme_handle_like(): void
{
    check_ajax_referer('mytheme_like', 'nonce');
 
    $postId = absint($_POST['post_id'] ?? 0);
 
    if (!$postId || get_post_status($postId) !== 'publish') {
        wp_send_json_error(['message' => 'Post not found'], 404);
    }
 
    $likes = (int) get_post_meta($postId, 'likes', true) + 1;
    update_post_meta($postId, 'likes', $likes);
 
    wp_send_json_success(['likes' => $likes]);
}

The wp_ajax_ hook fires for logged-in users, wp_ajax_nopriv_ for guests. For an action to work for everyone, both are attached.

The response

wp_send_json_success() and wp_send_json_error() print JSON and end the program at once:

{ "success": true, "data": { "likes": 12 } }

The address and the nonce for the script

The script needs to know the address of admin-ajax.php and the nonce. They are passed when the script is enqueued:

add_action('wp_enqueue_scripts', function () {
    wp_enqueue_script('mytheme-like', get_template_directory_uri() . '/assets/like.js', [], '1.0.0', true);
 
    wp_add_inline_script('mytheme-like', 'const mythemeLike = ' . wp_json_encode([
        'url' => admin_url('admin-ajax.php'),
        'nonce' => wp_create_nonce('mytheme_like'),
    ]) . ';', 'before');
});

The button

<?php
// template-parts/like.php
?>
<button class="like" data-post="<?php echo esc_attr(get_the_ID()); ?>">
    ♥ <span><?php echo (int) get_post_meta(get_the_ID(), 'likes', true); ?></span>
</button>

A request from JavaScript

// assets/like.js
document.querySelectorAll('.like').forEach((button) => {
    button.addEventListener('click', async () => {
        const body = new FormData();
        body.append('action', 'mytheme_like');
        body.append('nonce', mythemeLike.nonce);
        body.append('post_id', button.dataset.post);
 
        const response = await fetch(mythemeLike.url, { method: 'POST', body });
        const result = await response.json();
 
        if (result.success) {
            button.querySelector('span').textContent = result.data.likes;
        }
    });
});

The data is sent as a regular form: the handler reads it from $_POST.

Returning markup

For loading more posts a handler can return ready HTML:

add_action('wp_ajax_nopriv_mytheme_more', 'mytheme_handle_more');
add_action('wp_ajax_mytheme_more', 'mytheme_handle_more');
 
function mytheme_handle_more(): void
{
    $query = new WP_Query(['paged' => absint($_GET['page'] ?? 1)]);
 
    ob_start();
 
    while ($query->have_posts()) {
        $query->the_post();
        get_template_part('template-parts/card');
    }
 
    wp_send_json_success(['html' => ob_get_clean(), 'pages' => $query->max_num_pages]);
}

AJAX or the REST API

admin-ajax.phpREST API
AddressOne for every actionEach route has its own
Parameter validationBy handDescribed in the route
When to chooseSmall actions in a themeAn API for apps, many routes

Common mistake

If a request gets the response 0, the handler was not found. There are usually three causes: the request has no action parameter or it is named differently; the wp_ajax_nopriv_ hook is forgotten while you are testing as a guest; the add_action code sits in a page template and is not loaded on a request to admin-ajax.php — it belongs in functions.php or a plugin.

Thanks for reading!

Donate to the site — we work hard for you.