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.
- Enqueuing Scripts
WordPress manages JavaScript dependencies through a registration and enqueuing system. Never output <script> tags directly — always use the enqueue API.
Basic Pattern
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
wp_enqueue_scripts
Frontend pages None
admin_enqueue_scripts
Admin pages $hook_suffix (page filename)
login_enqueue_scripts
Login page None
Conditional Loading (Admin)
Load scripts only on your plugin's admin page — not on every admin screen:
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)
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
// 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).
- 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 wp_head() /wp_footer() fires.
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:
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 "5" , booleans become "" or "1" ).
-
For complex data, use wp_add_inline_script() with wp_json_encode() instead.
wp_add_inline_script()
Injects a <script> block tied to a registered handle. Supports 'before' or 'after' positioning (default: 'after' ).
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 'before' when your main script needs the config variable at load time.
Script Translation (i18n)
For Block Editor scripts using wp.i18n.__() :
wp_set_script_translations( 'map-editor', 'my-plugin', plugin_dir_path( FILE ) . 'languages' );
- Script Dependencies & Built-in Libraries
Common WordPress Script Handles
Handle Library Notes
jquery
jQuery (latest bundled) Runs in noConflict mode
jquery-core
jQuery core without migrate Lighter, no deprecated API shims
wp-api-fetch
@wordpress/api-fetch
REST API requests with nonce
wp-element
@wordpress/element (React wrapper) Block Editor components
wp-data
@wordpress/data
Block Editor state management
wp-hooks
@wordpress/hooks
JS action/filter system
wp-i18n
@wordpress/i18n
__() , _n() for JS
wp-util
wp.ajax , wp.template
AJAX helpers, Underscore templates
underscore
Underscore.js Utility library
backbone
Backbone.js MV* framework
wp-mediaelement
MediaElement.js Audio/video player
thickbox
ThickBox Modal dialogs
jQuery in WordPress
WordPress loads jQuery in noConflict mode — the $ shortcut is not available globally.
// 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
defer
Script executes after DOM is constructed, in order
async
Script executes immediately when downloaded, no order
(default) Blocking — halts parsing until script runs
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 async .
- AJAX
WordPress routes all AJAX requests through wp-admin/admin-ajax.php . The standard pattern: register wp_ajax_{action} (and optionally wp_ajax_nopriv_{action} ) hooks, verify the nonce with check_ajax_referer() , sanitize input, process data, and return a JSON response via wp_send_json_success() or wp_send_json_error() . On the client side, use jQuery.post() with the localized AJAX URL and action name.
Response Functions
Function Use Case
wp_send_json_success( $data )
{ success: true, data: $data }
wp_send_json_error( $data )
{ success: false, data: $data }
wp_send_json( $data )
Raw JSON (no success wrapper)
All three call wp_die() internally — do not echo or exit 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
wp_ajax_{action}
Logged-in users only
wp_ajax_nopriv_{action}
Non-logged-in users only
Both hooks registered All users
Register nopriv only if the endpoint must work for anonymous visitors. Always verify a nonce even on logged-in-only endpoints.
- Fetch API & wp.apiFetch (Modern Alternative)
For modern JavaScript (no jQuery dependency), use wp.apiFetch or the native Fetch API with the WordPress REST API instead of admin-ajax.php.
wp.apiFetch
// 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 wp-api-fetch as a dependency:
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
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
wp.apiFetch
Block Editor scripts, admin React UI
Native Fetch + REST Headless/decoupled, modern JS, custom REST routes
- 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 heartbeat-send event.
-
Server processes data through heartbeat_received filter.
-
Client receives response via heartbeat-tick event.
Hooks
Hook Type Side Purpose
heartbeat-send
Event JS Attach data before sending
heartbeat-tick
Event JS Process server response
heartbeat-error
Event JS Handle connection errors
heartbeat_received
Filter PHP Process data for logged-in users
heartbeat_nopriv_received
Filter PHP Process data for logged-out users
heartbeat_settings
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.
- wp.template (Underscore Templates)
WordPress bundles wp-util which provides wp.template() 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 admin_footer ), then render in JavaScript with wp.template( '{name}' ) . Requires wp-util as a script dependency.
Template Syntax
Syntax Purpose Example
{{ data.val }}
Escaped output (XSS-safe) {{ data.title }}
{{{ data.val }}}
Raw HTML output (trust the source) {{{ data.html }}}
<# code #>
JavaScript logic (if/for) <# if (data.x) { #>
- Common Mistakes
Mistake Fix
Outputting <script> tags directly Always use wp_enqueue_script()
Loading scripts on every admin page Check $hook_suffix in admin_enqueue_scripts callback
Using $ without jQuery wrapper Wrap in (function($) { ... })(jQuery); — noConflict mode
Hardcoding admin-ajax.php URL in JS Pass via wp_localize_script() using admin_url('admin-ajax.php')
Forgetting nonce in AJAX requests Always create with wp_create_nonce() , verify with check_ajax_referer()
Echoing after wp_send_json_*()
These call wp_die() internally — no code runs after them
Not registering nopriv hook for public AJAX Frontend AJAX for visitors needs wp_ajax_nopriv_{action}
Loading jQuery from CDN instead of bundled Use the WordPress handle jquery — prevents version conflicts
Using wp_localize_script() for non-string data Use wp_add_inline_script() with wp_json_encode() instead
Heartbeat running at 15s on non-editor pages Use heartbeat_settings 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