DEV Community

Cover image for No More Load On WordPress! Try Ajax Today!
MD Hemal Akhand
MD Hemal Akhand

Posted on

No More Load On WordPress! Try Ajax Today!

You build a WordPress form. It should save without reloading the page.

You click Send. Nothing happens.

Or the Network tab shows admin-ajax.php returning a single character: 0.

If that sounds familiar, you are not alone. After six years of building WordPress themes, plugins, and WooCommerce projects, I keep seeing the same pattern on client sites. The JavaScript looks fine. The PHP looks fine. But the request never reaches the callback — or it reaches the wrong hook — or the response is not JSON at all.

Most of the time, the problem is not JavaScript skill. It is not understanding how WordPress routes background requests.

This article walks through everything: what AJAX is in WordPress, how admin-ajax.php works, security with nonces, enqueueing scripts, frontend vs admin, JSON responses, debugging, and when to use REST instead. Copy the examples into your own plugin and change the prefixes.


What is AJAX in WordPress?

AJAX lets the browser talk to the server in the background without refreshing the page.

Real examples you have seen:

  • Submit a settings form and show "Saved!" instantly
  • Load more blog posts when someone clicks a button
  • Update a cart count in the header
  • Save a contact form without leaving the page

WordPress does not ask you to invent a custom API endpoint for every plugin. It gives you one built-in router:

/wp-admin/admin-ajax.php

You register an action name in PHP. JavaScript sends that same action in every POST request. WordPress connects the two — similar to add_action(), but triggered from the browser instead of a page load.


The one rule that breaks most projects

Every AJAX request must include:

action=your_action_name

WordPress reads that value and fires a hook:

  • wp_ajax_{action} — logged-in users only
  • wp_ajax_nopriv_{action} — logged-out visitors

If you register wp_ajax_send_form_data in PHP but JavaScript sends send-form-data, nothing runs.

If you only register wp_ajax_ but test while logged out on the public site, nothing runs.

If you forget the action key entirely, WordPress returns 0.

That is normal WordPress behavior. Once you expect it, debugging gets much faster.


The full request loop

Here is what happens on a working setup:

  1. User clicks Submit on a form
  2. JavaScript calls preventDefault() so the page does not reload
  3. JavaScript POSTs to admin-ajax.php with action, nonce, and your fields
  4. WordPress fires wp_ajax_{action} or wp_ajax_nopriv_{action}
  5. Your PHP callback runs, verifies the nonce, sanitizes input
  6. PHP returns JSON via wp_send_json_success() or wp_send_json_error()
  7. JavaScript reads response.success and updates the UI

If any step in that chain is wrong, the feature feels "broken" even when half the code is correct.


Part 1: Your first AJAX handler

Start with the smallest possible example: a logged-in admin request with no nonce yet (we add security in Part 2).

Register the action in PHP:

add_action( 'wp_ajax_send_form_data', 'myplugin_send_form_data_callback' );

function myplugin_send_form_data_callback() {
    wp_send_json_success(
        array(
            'message' => 'Hello from WordPress AJAX!',
        )
    );
}
Enter fullscreen mode Exit fullscreen mode

What to notice:

  • The action name is send_form_data — prefix it with your plugin name in real projects (myplugin_send_form_data) so you never collide with other plugins
  • JavaScript must send exactly action: 'send_form_data'
  • wp_send_json_success() prints JSON and stops execution — you do not need die() after it

Send the request from JavaScript inside wp-admin:

jQuery.ajax({
    method: 'POST',
    url: ajaxurl,
    data: {
        action: 'send_form_data',
    },
}).done(function (response) {
    console.log(response.data.message);
});
Enter fullscreen mode Exit fullscreen mode

Important detail: the global ajaxurl variable exists automatically in the WordPress admin area. It does not exist on the public frontend. For frontend forms, use wp_localize_script() — covered in Part 3.


Part 2: Secure AJAX with nonces

AJAX endpoints are public URLs. Anyone can send a POST request if they guess your action name.

For any operation that saves, deletes, or changes data, verify a nonce — a short-lived token WordPress generates to prove the request came from your site.

Step 1 — Create the nonce and pass it to JavaScript

wp_localize_script(
    'myplugin-admin-js',
    'my_ajax_obj',
    array(
        'ajax_url' => admin_url( 'admin-ajax.php' ),
        'nonce'    => wp_create_nonce( 'myplugin-ajax' ),
    )
);
Enter fullscreen mode Exit fullscreen mode

This creates a global JavaScript object my_ajax_obj with two values your script needs: the AJAX URL and the nonce string.

The script handle 'myplugin-admin-js' must match the handle you used in wp_enqueue_script().

The string 'myplugin-ajax' is your nonce action name — use the same string when verifying.

Step 2 — Send the nonce from JavaScript

data: {
    action: 'send_form_data',
    nonce: my_ajax_obj.nonce,
    user_name: 'Hemal',
}
Enter fullscreen mode Exit fullscreen mode

Step 3 — Verify in PHP before doing anything else

function myplugin_send_form_data_callback() {

    if ( ! isset( $_POST['nonce'] ) ) {
        wp_send_json_error(
            array(
                'message' => 'Nonce is missing.',
            )
        );
    }

    $nonce = sanitize_text_field( wp_unslash( $_POST['nonce'] ) );

    if ( ! wp_verify_nonce( $nonce, 'myplugin-ajax' ) ) {
        wp_send_json_error(
            array(
                'message' => 'Security check failed.',
            )
        );
    }

    $name = sanitize_text_field( wp_unslash( $_POST['user_name'] ?? '' ) );

    wp_send_json_success(
        array(
            'message' => 'Success!',
            'name'    => $name,
        )
    );
}
Enter fullscreen mode Exit fullscreen mode

Without a nonce, anyone on the internet can spam your endpoint. With a nonce, only requests that include a valid token from your enqueued script will pass verification.


Part 3: Enqueue scripts and pass data correctly

If your form lives on a custom admin page, you must load your JavaScript on that screen and pass the AJAX URL yourself.

add_action( 'admin_enqueue_scripts', 'myplugin_load_admin_scripts' );

function myplugin_load_admin_scripts() {

    wp_enqueue_script(
        'myplugin-admin-js',
        plugin_dir_url( __FILE__ ) . 'assets/js/admin.js',
        array( 'jquery' ),
        '1.0.0',
        true
    );

    wp_localize_script(
        'myplugin-admin-js',
        'my_ajax_obj',
        array(
            'ajax_url' => admin_url( 'admin-ajax.php' ),
            'nonce'    => wp_create_nonce( 'myplugin-ajax' ),
        )
    );
}
Enter fullscreen mode Exit fullscreen mode

What each part does:

Part Purpose
admin_enqueue_scripts Loads assets only in wp-admin
'myplugin-admin-js' Script handle — must match in wp_localize_script
array( 'jquery' ) Loads jQuery before your script
true at the end Loads script in footer (after DOM exists)
wp_localize_script Exposes my_ajax_obj.ajax_url and my_ajax_obj.nonce to JS
admin_url( 'admin-ajax.php' ) Full URL — works on local, staging, and subfolder installs

Never hard-code /wp-admin/admin-ajax.php. Always use admin_url().


Part 4: Put the pieces together

A clean plugin splits responsibilities:

  • Assets class — enqueue JS, pass nonce and URL
  • Admin page — render the HTML form
  • Ajax class — register wp_ajax_* hooks and callbacks

On the JavaScript side:

(function ($) {
    $(document).ready(function () {

        $('.myplugin-ajax-form').on('submit', function (event) {
            event.preventDefault();

            var $form = $(this);

            $.ajax({
                method: 'POST',
                url: my_ajax_obj.ajax_url,
                data: {
                    action: 'send_form_data',
                    nonce: my_ajax_obj.nonce,
                    user_name: $form.find('[name="user_name"]').val(),
                },
            }).done(function (response) {
                if (response.success) {
                    alert(response.data.message);
                } else {
                    alert(response.data.message || 'Request failed.');
                }
            });

        });

    });
})(jQuery);
Enter fullscreen mode Exit fullscreen mode

When the user clicks Send:

  1. Normal form submit is blocked
  2. POST goes to admin-ajax.php
  3. WordPress reads action=send_form_data
  4. Hook wp_ajax_send_form_data fires
  5. PHP verifies nonce and returns JSON
  6. JavaScript shows the result

That is the entire system. Nothing hidden.


Part 5: JSON responses — success and error

Use WordPress helpers. Do not echo random text and call die() — it breaks JSON parsing in JavaScript.

Success:

wp_send_json_success(
    array(
        'message' => 'Saved!',
        'id'      => 123,
    )
);
Enter fullscreen mode Exit fullscreen mode

Error:

wp_send_json_error(
    array(
        'message' => 'Something went wrong.',
    ),
    400
);
Enter fullscreen mode Exit fullscreen mode

JavaScript receives:

{
    "success": true,
    "data": {
        "message": "Saved!",
        "id": 123
    }
}
Enter fullscreen mode Exit fullscreen mode

Always check response.success before reading response.data.

Always sanitize before save:

$name  = sanitize_text_field( wp_unslash( $_POST['user_name'] ?? '' ) );
$email = sanitize_email( wp_unslash( $_POST['email'] ?? '' ) );
$id    = absint( $_POST['item_id'] ?? 0 );
Enter fullscreen mode Exit fullscreen mode

Part 6: Frontend AJAX for logged-out users

Public contact forms need the nopriv hook.

Register both with the same callback:

add_action( 'wp_ajax_send_form_data', 'myplugin_send_form_data_callback' );
add_action( 'wp_ajax_nopriv_send_form_data', 'myplugin_send_form_data_callback' );
Enter fullscreen mode Exit fullscreen mode
Hook When it runs
wp_ajax_{action} User is logged in
wp_ajax_nopriv_{action} User is not logged in

This is the most common "works for me, broken for clients" bug. You test as admin. Real visitors are logged out.

For admin-only actions (delete post, save settings), skip nopriv and add current_user_can() inside the callback instead.


Part 7: Enqueue on the frontend

Admin uses admin_enqueue_scripts. The public site uses wp_enqueue_scripts:

add_action( 'wp_enqueue_scripts', 'myplugin_load_public_scripts' );

function myplugin_load_public_scripts() {

    wp_enqueue_script(
        'myplugin-public-js',
        plugin_dir_url( __FILE__ ) . 'assets/js/public.js',
        array( 'jquery' ),
        '1.0.0',
        true
    );

    wp_localize_script(
        'myplugin-public-js',
        'my_ajax_obj',
        array(
            'ajax_url' => admin_url( 'admin-ajax.php' ),
            'nonce'    => wp_create_nonce( 'myplugin-ajax' ),
        )
    );
}
Enter fullscreen mode Exit fullscreen mode

Same pattern. Different hook. If the script is not enqueued on the page where your form appears, the JavaScript never runs — even if PHP is perfect.


Part 8: Using fetch() instead of jQuery

WordPress does not care which library you use. It cares about POST, action, and nonce.

document.querySelector('.myplugin-ajax-form')?.addEventListener('submit', async function (event) {

    event.preventDefault();

    var form = event.target;
    var formData = new FormData();

    formData.append('action', 'send_form_data');
    formData.append('nonce', my_ajax_obj.nonce);
    formData.append('user_name', form.querySelector('[name="user_name"]').value);

    var response = await fetch(my_ajax_obj.ajax_url, {
        method: 'POST',
        body: formData,
        credentials: 'same-origin',
    });

    var json = await response.json();

    if (json.success) {
        alert(json.data.message);
    } else {
        alert(json.data.message || 'Request failed.');
    }

});
Enter fullscreen mode Exit fullscreen mode

Part 9: admin-ajax.php vs REST API

WordPress offers two async patterns:

admin-ajax.php

  • URL: /wp-admin/admin-ajax.php
  • Register with add_action( 'wp_ajax_*' )
  • Best for classic plugins, wp-admin UI, small forms

REST API

  • URL: /wp-json/your-namespace/v1/your-route
  • Register with register_rest_route()
  • Best for headless sites, Gutenberg blocks, mobile apps

For most theme and plugin work inside WordPress admin, admin-ajax.php is still the fastest path. Move to REST when you need standard HTTP routes, external consumers, or block editor integration.


Part 10: Debugging when AJAX fails

Check these in order before guessing:

  1. Action name — identical string in PHP hook and JavaScript action field?
  2. Network tab — open DevTools, find admin-ajax.php, read the response body
  3. Logged out? — did you register wp_ajax_nopriv_?
  4. Script loaded? — is wp_enqueue_script running on this exact page?
  5. Nonce — created, passed to JS, sent back, verified with same action string?

Common responses:

Response Meaning
0 Action not registered or callback not found
-1 Nonce failed or permission denied
JSON with "success": false Callback ran but returned an error
Empty or HTML PHP error, or you used echo instead of JSON helpers

Temporary logging in PHP:

error_log( print_r( $_POST, true ) );
Enter fullscreen mode Exit fullscreen mode

Enable WP_DEBUG_LOG in wp-config.php and read wp-content/debug.log.


Quick reference

I want to… Use
Handle logged-in AJAX add_action( 'wp_ajax_{action}', 'callback' )
Handle logged-out AJAX add_action( 'wp_ajax_nopriv_{action}', 'callback' )
Pass URL + nonce to JS wp_localize_script()
Return success JSON wp_send_json_success( $data )
Return error JSON wp_send_json_error( $data )
Verify token wp_verify_nonce( $nonce, 'action-name' )
AJAX router URL admin_url( 'admin-ajax.php' )

Common mistakes

  1. Missing action in JavaScript — WordPress returns 0
  2. Mismatched action names between PHP and JS
  3. Forgetting wp_ajax_nopriv_ on public forms
  4. No nonce on write operations
  5. Using echo instead of wp_send_json_*
  6. Hard-coding /wp-admin/admin-ajax.php
  7. Not sanitizing $_POST
  8. Scheduling script on wrong hook or wrong admin page

Checklist before shipping

  • Action name is unique and prefixed (myplugin_send_form_data)
  • wp_ajax_ registered (and wp_ajax_nopriv_ if public)
  • Nonce created, passed to JS, verified in PHP
  • Input sanitized, output escaped
  • wp_send_json_success / wp_send_json_error used
  • Script enqueued on the correct hook and page
  • Tested logged in and logged out
  • Network tab checked for 0, -1, or 500 errors

What you can build with this

Once the loop is clear, many features become straightforward:

  • Live search and filters
  • Infinite scroll / load more
  • Theme option panels that save instantly
  • Newsletter and contact forms
  • Admin dashboards that refresh without reload
  • WooCommerce-style cart updates

Same WordPress hook thinking — triggered from JavaScript.


Read more

Medium — full walkthrough with additional examples:

https://medium.com/@hemal.akanda.39/no-refresh-in-wordpress-by-ajax-4942c5e80bd2

Substack — same topic in plain English, no code:

https://substack.com/@mdhemalakhand/note/p-201821073


What I am building now

After years of WordPress client work, I am building AppsLaunch — branded Android and iPhone apps for local service businesses from one dashboard.

How it works:

  • Create your company and add logo, colors, services, and products
  • Customers browse in the app and send inquiries — leads land in your dashboard inbox
  • Build signed Android and iPhone apps from the cloud and publish to Google Play and the App Store
  • Each business gets its own app name and store listing — not a generic builder brand

Plans from $30/month. No per-lead fees on paid plans.

https://applaunch.teamzlab.com/


Final thought

WordPress AJAX is four ideas on every project:

  1. Register the action in PHP
  2. Send the same action name from JavaScript
  3. Secure the request with nonces and sanitization
  4. Return clean JSON

Once you see that loop, no-refresh forms stop feeling like magic and start feeling like normal WordPress development.

Top comments (0)