WordPress JavaScript & AJAX
Reference for JavaScript integration in WordPress: script registration and enqueuing,
passing data from PHP to JS, AJAX request/response patterns, the Heartbeat API,
jQuery usage, and loading strategies.
1. Enqueuing Scripts
WordPress manages JavaScript dependencies through a registration and enqueuing system.
Never output
tags directly — always use the enqueue API.
Basic Pattern
php
add_action( 'wp_enqueue_scripts', 'map_enqueue_frontend_scripts' );
function map_enqueue_frontend_scripts(): void {
wp_enqueue_script(
'map-frontend', // Handle (unique identifier).
plugins_url( 'assets/js/frontend.js', __FILE__ ), // Full URL to file.
array( 'jquery' ), // Dependencies.
'1.0.0', // Version (cache busting).
array( 'in_footer' => true ) // Load in footer.
);
}
Enqueue Hooks
| Hook | Where Scripts Load | Callback Parameter |
|---|
| Frontend pages | None |
| Admin pages | (page filename) |
| Login page | None |
Conditional Loading (Admin)
Load scripts only on your plugin's admin page — not on every admin screen:
php
add_action( 'admin_enqueue_scripts', 'map_enqueue_admin_scripts' );
function map_enqueue_admin_scripts( string $hook_suffix ): void {
// $hook_suffix examples: 'toplevel_page_my-plugin', 'settings_page_my-plugin-settings'.
if ( 'toplevel_page_my-plugin' !== $hook_suffix ) {
return;
}
wp_enqueue_script(
'map-admin',
plugins_url( 'assets/js/admin.js', __FILE__ ),
array( 'jquery', 'wp-util' ),
MAP_VERSION,
array( 'in_footer' => true )
);
}
Conditional Loading (Frontend)
php
add_action( 'wp_enqueue_scripts', 'map_enqueue_conditionally' );
function map_enqueue_conditionally(): void {
// Only load on single posts.
if ( ! is_single() ) {
return;
}
wp_enqueue_script( 'map-single', /* ... */ );
}
Register vs Enqueue
php
// Register (makes handle available, doesn't load yet).
wp_register_script( 'map-charts', plugins_url( 'assets/js/charts.js', __FILE__ ), array(), '2.0.0', true );
// Enqueue later when needed (e.g., inside a shortcode callback).
function map_chart_shortcode( $atts ): string {
wp_enqueue_script( 'map-charts' ); // Now it loads.
return '<div id="map-chart"></div>';
}
Registration is useful when a script should only load conditionally (inside a
shortcode, meta box, or specific template).
2. Passing Data from PHP to JavaScript
wp_localize_script()
Passes PHP values to JavaScript as a global object. Must be called after the
script is enqueued/registered and before
/
fires.
php
wp_enqueue_script( 'map-ajax', plugins_url( 'assets/js/ajax.js', __FILE__ ), array( 'jquery' ), '1.0.0', true );
wp_localize_script( 'map-ajax', 'mapAjax', array(
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'map_ajax_nonce' ),
'homeUrl' => home_url( '/' ),
'i18n' => array(
'loading' => __( 'Loading...', 'my-plugin' ),
'error' => __( 'An error occurred.', 'my-plugin' ),
),
) );
In JavaScript, the data is available as a global:
js
console.log( mapAjax.ajaxUrl ); // "https://example.com/wp-admin/admin-ajax.php"
console.log( mapAjax.nonce ); // "a1b2c3d4e5"
console.log( mapAjax.i18n.loading ); // "Loading..."
Limitations:
- All values are cast to strings (numbers become , booleans become or ).
- For complex data, use with instead.
wp_add_inline_script()
Injects a
block tied to a registered handle. Supports
or
positioning (default:
).
php
wp_enqueue_script( 'map-app', plugins_url( 'assets/js/app.js', __FILE__ ), array(), '1.0.0', true );
wp_add_inline_script( 'map-app', sprintf(
'var mapConfig = %s;',
wp_json_encode( array(
'apiBase' => rest_url( 'map/v1/' ),
'nonce' => wp_create_nonce( 'wp_rest' ),
'maxItems' => 50, // Stays as integer.
'debug' => WP_DEBUG, // Stays as boolean.
) )
), 'before' );
Use
when your main script needs the config variable at load time.
Script Translation (i18n)
For Block Editor scripts using
:
php
wp_set_script_translations( 'map-editor', 'my-plugin', plugin_dir_path( __FILE__ ) . 'languages' );
3. Script Dependencies & Built-in Libraries
Common WordPress Script Handles
| Handle | Library | Notes |
|---|
| jQuery (latest bundled) | Runs in noConflict mode |
| jQuery core without migrate | Lighter, no deprecated API shims |
| | REST API requests with nonce |
| (React wrapper) | Block Editor components |
| | Block Editor state management |
| | JS action/filter system |
| | , for JS |
| , | AJAX helpers, Underscore templates |
| Underscore.js | Utility library |
| Backbone.js | MV* framework |
| MediaElement.js | Audio/video player |
| ThickBox | Modal dialogs |
jQuery in WordPress
WordPress loads jQuery in
noConflict mode — the
shortcut is not available globally.
js
// WRONG — $ is undefined in noConflict mode.
$( '.my-element' ).hide();
// Option 1: Use the full name.
jQuery( '.my-element' ).hide();
// Option 2: Wrap in an IIFE (recommended).
( function( $ ) {
$( '.my-element' ).hide();
$( document ).ready( function() {
// DOM ready code.
} );
} )( jQuery );
// Option 3: jQuery ready shorthand.
jQuery( function( $ ) {
$( '.my-element' ).hide();
} );
Loading Strategies (WordPress 6.3+)
| Strategy | Behavior |
|---|
| Script executes after DOM is constructed, in order |
| Script executes immediately when downloaded, no order |
| (default) | Blocking — halts parsing until script runs |
php
wp_enqueue_script( 'map-analytics', plugins_url( 'assets/js/analytics.js', __FILE__ ), array(), '1.0.0', array(
'in_footer' => true,
'strategy' => 'defer', // Non-blocking, ordered execution.
) );
WordPress automatically adjusts strategies to prevent dependency conflicts. If a
dependency uses blocking, the dependent script cannot use
.
4. AJAX
WordPress routes all AJAX requests through
. The standard
pattern: register
(and optionally
) hooks,
verify the nonce with
, sanitize input, process data, and return
a JSON response via
or
. On the client
side, use
with the localized AJAX URL and action name.
<!-- Full PHP handler and jQuery client examples: resources/ajax-patterns.md -->
Response Functions
| Function | Use Case |
|---|
wp_send_json_success( $data )
| { success: true, data: $data }
|
wp_send_json_error( $data )
| { success: false, data: $data }
|
| Raw JSON (no success wrapper) |
All three call
internally — do not
or
after them.
Nonce Verification
| Function | Use Case |
|---|
check_ajax_referer( $action, $key )
| Dies on failure (use for AJAX handlers) |
wp_verify_nonce( $_POST['nonce'], $action )
| Returns false on failure (manual check) |
Admin-Only vs Public AJAX
| Hook Pattern | Who Can Call |
|---|
| Logged-in users only |
| Non-logged-in users only |
| Both hooks registered | All users |
Register
only if the endpoint must work for anonymous visitors.
Always verify a nonce even on logged-in-only endpoints.
5. Fetch API & wp.apiFetch (Modern Alternative)
For modern JavaScript (no jQuery dependency), use
or the native
Fetch API with the WordPress REST API instead of admin-ajax.php.
wp.apiFetch
js
// Automatically includes X-WP-Nonce header.
wp.apiFetch( { path: '/wp/v2/posts?per_page=5' } )
.then( function( posts ) {
console.log( posts );
} );
// POST request.
wp.apiFetch( {
path: '/map/v1/settings',
method: 'POST',
data: { key: 'value' },
} ).then( function( response ) {
console.log( response );
} );
Requires
as a dependency:
php
wp_enqueue_script( 'map-modern', plugins_url( 'assets/js/modern.js', __FILE__ ), array( 'wp-api-fetch' ), '1.0.0', true );
Native Fetch with REST API
js
fetch( mapConfig.apiBase + 'settings', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': mapConfig.nonce,
},
body: JSON.stringify( { key: 'value' } ),
} )
.then( r => r.json() )
.then( data => console.log( data ) );
When to Use What
| Approach | Best For |
|---|
| admin-ajax.php | Legacy code, simple form submissions, no REST API |
| Block Editor scripts, admin React UI |
| Native Fetch + REST | Headless/decoupled, modern JS, custom REST routes |
6. Heartbeat API
The Heartbeat API provides near-real-time server polling. WordPress uses it for
post locking, autosave, and login session management.
How It Works
- Browser fires a "tick" every 15-120 seconds (default: 60s on most pages, 15s on post editor).
- Client-side code attaches data via event.
- Server processes data through filter.
- Client receives response via event.
<!-- Full send/receive code, interval control, and disabling examples: resources/heartbeat-templates.md -->
Hooks
| Hook | Type | Side | Purpose |
|---|
| Event | JS | Attach data before sending |
| Event | JS | Process server response |
| Event | JS | Handle connection errors |
| Filter | PHP | Process data for logged-in users |
heartbeat_nopriv_received
| Filter | PHP | Process data for logged-out users |
| Filter | PHP | Modify heartbeat interval |
Use Cases
- Post locking — warn when another user is editing the same post.
- Autosave — periodically save draft content.
- Session management — extend logged-in session while user is active.
- Real-time notifications — poll for new data without WebSockets.
- Dashboard widgets — auto-refresh stats or activity feeds.
7. wp.template (Underscore Templates)
WordPress bundles
which provides
for client-side
HTML rendering using Underscore.js template syntax. Define templates in PHP
using
<script type="text/html" id="tmpl-{name}">
blocks (typically in
), then render in JavaScript with
.
Requires
as a script dependency.
Template Syntax
| Syntax | Purpose | Example |
|---|
| Escaped output (XSS-safe) | |
| Raw HTML output (trust the source) | |
| JavaScript logic (if/for) | |
<!-- Full PHP template definition and JS rendering examples: resources/heartbeat-templates.md -->
8. Common Mistakes
| Mistake | Fix |
|---|
| Outputting tags directly | Always use |
| Loading scripts on every admin page | Check in callback |
| Using without jQuery wrapper | Wrap in (function($) { ... })(jQuery);
— noConflict mode |
| Hardcoding URL in JS | Pass via using admin_url('admin-ajax.php')
|
| Forgetting nonce in AJAX requests | Always create with , verify with |
| Echoing after | These call internally — no code runs after them |
| Not registering hook for public AJAX | Frontend AJAX for visitors needs |
| Loading jQuery from CDN instead of bundled | Use the WordPress handle — prevents version conflicts |
| Using for non-string data | Use with instead |
| Heartbeat running at 15s on non-editor pages | Use filter to slow it down or disable |
| Not declaring script dependencies | List all deps so WordPress loads them in correct order |
| Mixing admin-ajax and REST API unnecessarily | Pick one approach per feature — REST API for new code |