The WordPress Interactivity API provides a declarative approach to event handling through the data-wp-on directive, eliminating the need for manual addEventListener calls and ensuring proper clean up. Whether you’re capturing clicks, keyboard input, form submissions, or touch gestures, data-wp-on offers a consistent pattern that integrates seamlessly with your state management architecture.
In this article, we’ll explore comprehensive event handling patterns—from basic interactions to advanced techniques like event delegation, preventing defaults, accessing event properties asynchronously, and implementing complex user interaction flows across your WordPress blocks.
Understanding data-wp-on: The Foundation
The data-wp-on directive connects DOM events to actions in your store, creating a reactive bridge between user interactions and state updates.
Basic Syntax and Event Registration
The fundamental pattern follows this structure:
<button
data-wp-interactive="myPlugin"
data-wp-on--click="actions.handleClick"
>
Click Me
</button>Key Concepts:
data-wp-on--[eventName]defines which event to listen for- The value references an action in your store
- Event listeners are automatically registered during activation
- Clean up is handled automatically when elements are removed
Defining Actions in Your Store
Actions receive the native DOM event object as their first parameter:
// view.js
import { store } from '@wordpress/interactivity';
store( 'myPlugin', {
state: {
clickCount: 0,
lastClickTime: null
},
actions: {
handleClick( event ) {
const { state } = store( 'myPlugin' );
// Access event properties
console.log( 'Button clicked at:', event.clientX, event.clientY );
// Update state
state.clickCount += 1;
state.lastClickTime = new Date().toISOString();
}
}
} );The action receives the full event object, providing access to properties like target, currentTarget, clientX, clientY, and methods like preventDefault() and stopPropagation().
Common Event Types and Patterns
Mouse Events
Handle clicks, hovers, and mouse movements with these common events:
<div data-wp-interactive="myPlugin">
<!-- Click Events -->
<button data-wp-on--click="actions.handleClick">
Click
</button>
<!-- Double Click -->
<button data-wp-on--dblclick="actions.handleDoubleClick">
Double Click
</button>
<!-- Mouse Enter/Leave (for hover effects) -->
<div
data-wp-on--mouseenter="actions.handleMouseEnter"
data-wp-on--mouseleave="actions.handleMouseLeave"
data-wp-class--hovered="state.isHovered"
>
Hover over me
</div>
<!-- Mouse Move (for tracking cursor position) -->
<div
data-wp-on--mousemove="actions.trackMousePosition"
class="canvas"
>
<span>X: <span data-wp-text="state.mouseX"></span></span>
<span>Y: <span data-wp-text="state.mouseY"></span></span>
</div>
</div>store( 'myPlugin', {
state: {
isHovered: false,
mouseX: 0,
mouseY: 0,
clickCount: 0
},
actions: {
handleClick() {
const { state } = store( 'myPlugin' );
state.clickCount += 1;
},
handleDoubleClick() {
const { state } = store( 'myPlugin' );
console.log( 'Double clicked!' );
},
handleMouseEnter() {
const { state } = store( 'myPlugin' );
state.isHovered = true;
},
handleMouseLeave() {
const { state } = store( 'myPlugin' );
state.isHovered = false;
},
trackMousePosition( event ) {
const { state } = store( 'myPlugin' );
state.mouseX = event.clientX;
state.mouseY = event.clientY;
}
}
} );Keyboard Events
Capture keyboard input for shortcuts, navigation, and form interactions:
<div data-wp-interactive="myPlugin">
<!-- Key Press Detection -->
<input
type="text"
data-wp-on--keydown="actions.handleKeyDown"
data-wp-on--keyup="actions.handleKeyUp"
placeholder="Type something..."
/>
<!-- Keyboard Shortcut (Ctrl/Cmd + S) -->
<div
data-wp-on--keydown="actions.handleSaveShortcut"
tabindex="0"
>
Press Ctrl/Cmd + S to save
</div>
<!-- Enter Key Submission -->
<input
type="text"
data-wp-bind--value="state.searchQuery"
data-wp-on--keydown="actions.handleSearchKeyDown"
placeholder="Search (press Enter)"
/>
</div>store( 'myPlugin', {
state: {
searchQuery: '',
lastKey: '',
isTyping: false
},
actions: {
handleKeyDown( event ) {
const { state } = store( 'myPlugin' );
state.lastKey = event.key;
state.isTyping = true;
},
handleKeyUp() {
const { state } = store( 'myPlugin' );
state.isTyping = false;
},
handleSaveShortcut( event ) {
const { actions } = store( 'myPlugin' );
// Check for Ctrl+S or Cmd+S
if ( ( event.ctrlKey || event.metaKey ) && event.key === 's' ) {
event.preventDefault(); // Prevent browser save dialog
actions.saveData();
}
},
handleSearchKeyDown( event ) {
const { actions } = store( 'myPlugin' );
if ( event.key === 'Enter' ) {
event.preventDefault();
actions.performSearch();
}
},
performSearch() {
const { state } = store( 'myPlugin' );
console.log( 'Searching for:', state.searchQuery );
},
saveData() {
console.log( 'Saving data...' );
}
}
} );Accessibility Note: When implementing keyboard shortcuts, always provide alternative methods (buttons, menu items) to ensure accessibility for all users.
Form Events
Handle form interactions, validation, and submissions:
<form
data-wp-interactive="myPlugin"
data-wp-on--submit="actions.handleSubmit"
>
<!-- Input Change Events -->
<input
type="text"
name="username"
data-wp-bind--value="state.formData.username"
data-wp-on--input="actions.handleUsernameInput"
data-wp-on--blur="actions.validateUsername"
/>
<span
data-wp-class--visible="state.errors.username"
data-wp-text="state.errors.username"
class="error"
></span>
<!-- Checkbox Change -->
<label>
<input
type="checkbox"
data-wp-bind--checked="state.formData.subscribe"
data-wp-on--change="actions.handleSubscribeChange"
/>
Subscribe to newsletter
</label>
<!-- Select Change -->
<select
data-wp-bind--value="state.formData.country"
data-wp-on--change="actions.handleCountryChange"
>
<option value="">Select country</option>
<option value="us">United States</option>
<option value="uk">United Kingdom</option>
<option value="ca">Canada</option>
</select>
<!-- Submit Button -->
<button
type="submit"
data-wp-bind--disabled="state.isSubmitting"
>
<span data-wp-text="state.isSubmitting ? 'Submitting...' : 'Submit'"></span>
</button>
</form>store( 'myPlugin', {
state: {
formData: {
username: '',
subscribe: false,
country: ''
},
errors: {
username: ''
},
isSubmitting: false
},
actions: {
handleUsernameInput( event ) {
const { state } = store( 'myPlugin' );
state.formData.username = event.target.value;
// Clear error when user starts typing
if ( state.errors.username ) {
state.errors.username = '';
}
},
validateUsername() {
const { state } = store( 'myPlugin' );
const username = state.formData.username.trim();
if ( username.length === 0 ) {
state.errors.username = 'Username is required';
} else if ( username.length < 3 ) {
state.errors.username = 'Username must be at least 3 characters';
} else {
state.errors.username = '';
}
},
handleSubscribeChange( event ) {
const { state } = store( 'myPlugin' );
state.formData.subscribe = event.target.checked;
},
handleCountryChange( event ) {
const { state } = store( 'myPlugin' );
state.formData.country = event.target.value;
},
async handleSubmit( event ) {
event.preventDefault(); // Prevent default form submission
const { state, actions } = store( 'myPlugin' );
// Validate all fields
actions.validateUsername();
if ( state.errors.username ) {
return;
}
state.isSubmitting = true;
try {
const response = await fetch( '/wp-json/myplugin/v1/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify( state.formData )
} );
const result = await response.json();
console.log( 'Form submitted successfully:', result );
// Reset form
state.formData = {
username: '',
subscribe: false,
country: ''
};
} catch ( error ) {
console.error( 'Form submission failed:', error );
} finally {
state.isSubmitting = false;
}
}
}
} );Form Handling Best Practices:
- Always call
event.preventDefault()on form submissions to prevent page reloads - Validate on both
blur(when user leaves field) andsubmitevents - Provide immediate feedback for validation errors
- Disable submit button during async operations
Touch Events
Support mobile interactions with touch event handlers:
<div
data-wp-interactive="myPlugin"
data-wp-on--touchstart="actions.handleTouchStart"
data-wp-on--touchmove="actions.handleTouchMove"
data-wp-on--touchend="actions.handleTouchEnd"
class="swipeable"
>
Swipe me
<div data-wp-text="state.swipeDirection"></div>
</div>store( 'myPlugin', {
state: {
touchStartX: 0,
touchStartY: 0,
swipeDirection: ''
},
actions: {
handleTouchStart( event ) {
const { state } = store( 'myPlugin' );
const touch = event.touches[ 0 ];
state.touchStartX = touch.clientX;
state.touchStartY = touch.clientY;
},
handleTouchMove( event ) {
// Optionally prevent scrolling during swipe
// event.preventDefault();
},
handleTouchEnd( event ) {
const { state } = store( 'myPlugin' );
const touch = event.changedTouches[ 0 ];
const deltaX = touch.clientX - state.touchStartX;
const deltaY = touch.clientY - state.touchStartY;
const minSwipeDistance = 50;
if ( Math.abs( deltaX ) > Math.abs( deltaY ) ) {
// Horizontal swipe
if ( Math.abs( deltaX ) > minSwipeDistance ) {
state.swipeDirection = deltaX > 0 ? 'right' : 'left';
}
} else {
// Vertical swipe
if ( Math.abs( deltaY ) > minSwipeDistance ) {
state.swipeDirection = deltaY > 0 ? 'down' : 'up';
}
}
}
}
} );Advanced Event Handling Patterns
Preventing Default Behaviour and Stopping Propagation
For synchronous event property access, use withSyncEvent():
import { store, withSyncEvent } from '@wordpress/interactivity';
store( 'myPlugin', {
actions: {
// Synchronous event handling for preventDefault
preventDefaultLink: withSyncEvent( ( event ) => {
event.preventDefault();
console.log( 'Link click prevented' );
} ),
// Stop event propagation
stopPropagation: withSyncEvent( ( event ) => {
event.stopPropagation();
console.log( 'Event stopped from bubbling' );
} ),
// Combined: prevent default and stop propagation
handleCustomBehavior: withSyncEvent( ( event ) => {
event.preventDefault();
event.stopPropagation();
const { actions } = store( 'myPlugin' );
actions.performCustomAction();
} ),
performCustomAction() {
console.log( 'Custom action performed' );
}
}
} );<div data-wp-interactive="myPlugin">
<!-- Prevent default link navigation -->
<a
href="/some-page"
data-wp-on--click="actions.preventDefaultLink"
>
Click (won't navigate)
</a>
<!-- Stop event propagation -->
<div data-wp-on--click="actions.handleParentClick">
Parent
<button data-wp-on--click="actions.stopPropagation">
Child (won't trigger parent)
</button>
</div>
</div>Important: Use withSyncEvent() when you need to call preventDefault(), stopPropagation(), or access event properties like event.key before the action completes. This ensures synchronous event handling. See [Article 5: State Flow](The Interactivity API State Flow_ A Comprehensive Guide for Large-Scale Applications.md) for more on withSyncEvent().
Accessing Context and Element Data
Use getContext() and getElement() to access contextual data and DOM references:
import { store, getContext, getElement } from '@wordpress/interactivity';
store( 'myPlugin', {
actions: {
handleItemClick() {
const context = getContext();
const element = getElement();
// Access item data from context
console.log( 'Clicked item:', context.item );
// Access DOM element
console.log( 'Element:', element.ref );
// Read data attributes
const itemId = element.ref.dataset.itemId;
console.log( 'Item ID:', itemId );
}
}
} );<div data-wp-interactive="myPlugin">
<template data-wp-each="state.items">
<button
data-wp-on--click="actions.handleItemClick"
data-wp-bind--data-item-id="context.item.id"
>
<span data-wp-text="context.item.name"></span>
</button>
</template>
</div>This pattern is especially useful within data-wp-each loops where you need to identify which item was clicked. See [Article 6: Building Dynamic Lists](Building Dynamic Lists and Collections with data-wp-each.md) for more list interaction patterns.
Event Delegation and Dynamic Content
The Interactivity API automatically handles event delegation—events on dynamically added elements work without re-registering listeners:
store( 'myPlugin', {
state: {
items: [ { id: 1, name: 'Item 1' } ]
},
actions: {
addItem() {
const { state } = store( 'myPlugin' );
state.items = [
...state.items,
{
id: Date.now(),
name: `Item ${ state.items.length + 1 }`
}
];
},
removeItem() {
const context = getContext();
const { state } = store( 'myPlugin' );
// Remove item by filtering
state.items = state.items.filter(
item => item.id !== context.item.id
);
}
}
} );<div data-wp-interactive="myPlugin">
<button data-wp-on--click="actions.addItem">
Add Item
</button>
<ul>
<template data-wp-each="state.items">
<li>
<span data-wp-text="context.item.name"></span>
<!-- This event handler works even for newly added items -->
<button data-wp-on--click="actions.removeItem">
Remove
</button>
</li>
</template>
</ul>
</div>Key Advantage: Unlike manual addEventListener, you don’t need to worry about attaching events to new DOM elements—the Interactivity API handles this automatically.
Debouncing and Throttling Events
For high-frequency events like scroll, resize, or input, implement debouncing or throttling:
store( 'myPlugin', {
state: {
searchQuery: '',
searchResults: [],
scrollPosition: 0,
isSearching: false
},
actions: {
// Debounced search (waits for user to stop typing)
handleSearchInput( event ) {
const { state, actions } = store( 'myPlugin' );
state.searchQuery = event.target.value;
// Clear existing timeout
if ( state.searchTimeout ) {
clearTimeout( state.searchTimeout );
}
// Set new timeout
state.searchTimeout = setTimeout( () => {
actions.performSearch();
}, 300 ); // Wait 300ms after user stops typing
},
async performSearch() {
const { state } = store( 'myPlugin' );
if ( ! state.searchQuery.trim() ) {
state.searchResults = [];
return;
}
state.isSearching = true;
try {
const response = await fetch(
`/wp-json/myplugin/v1/search?q=${ encodeURIComponent( state.searchQuery ) }`
);
const results = await response.json();
state.searchResults = results;
} catch ( error ) {
console.error( 'Search failed:', error );
} finally {
state.isSearching = false;
}
},
// Throttled scroll (limits execution frequency)
handleScroll( event ) {
const { state, actions } = store( 'myPlugin' );
// Only execute if enough time has passed
if ( state.scrollThrottleTimeout ) {
return;
}
state.scrollPosition = event.target.scrollTop;
// Set throttle timeout
state.scrollThrottleTimeout = setTimeout( () => {
state.scrollThrottleTimeout = null;
}, 100 ); // Execute at most once per 100ms
}
}
} );<div data-wp-interactive="myPlugin">
<!-- Debounced search input -->
<input
type="search"
data-wp-bind--value="state.searchQuery"
data-wp-on--input="actions.handleSearchInput"
placeholder="Search..."
/>
<div data-wp-class--visible="state.isSearching">
Searching...
</div>
<!-- Throttled scroll handler -->
<div
data-wp-on--scroll="actions.handleScroll"
class="scrollable-container"
>
<p>Scroll position: <span data-wp-text="state.scrollPosition"></span></p>
</div>
</div>Performance Tip: Debouncing is ideal for actions triggered by user input (search, validation), while throttling works better for continuous events (scroll, resize, mouse move).
Real-World Implementation: Interactive To-do List
Let’s combine event handling patterns into a complete to-do list with keyboard shortcuts, form validation, and item management:
<?php
// render.php
$initial_todos = array(
array( 'id' => 1, 'title' => 'Learn Interactivity API', 'completed' => true ),
array( 'id' => 2, 'title' => 'Build interactive block', 'completed' => false ),
);
wp_interactivity_state( 'myPlugin', array(
'todos' => $initial_todos,
'newTodoTitle' => '',
'filter' => 'all', // all, active, completed
'editingId' => null,
'editingTitle' => '',
) );
?>
<div
data-wp-interactive="myPlugin"
class="todo-app"
data-wp-on--keydown="actions.handleGlobalKeyDown"
>
<!-- Add Todo Form -->
<form data-wp-on--submit="actions.handleAddTodo">
<input
type="text"
data-wp-bind--value="state.newTodoTitle"
data-wp-on--input="actions.handleNewTodoInput"
data-wp-on--keydown="actions.handleNewTodoKeyDown"
placeholder="What needs to be done?"
class="new-todo-input"
/>
<button type="submit">Add</button>
</form>
<!-- Filter Buttons -->
<div class="filters">
<button
data-wp-on--click="actions.setFilter"
data-wp-bind--data-filter="all"
data-wp-class--active="state.filter === 'all'"
>
All (<span data-wp-text="state.todos.length"></span>)
</button>
<button
data-wp-on--click="actions.setFilter"
data-wp-bind--data-filter="active"
data-wp-class--active="state.filter === 'active'"
>
Active (<span data-wp-text="state.activeTodosCount"></span>)
</button>
<button
data-wp-on--click="actions.setFilter"
data-wp-bind--data-filter="completed"
data-wp-class--active="state.filter === 'completed'"
>
Completed (<span data-wp-text="state.completedTodosCount"></span>)
</button>
</div>
<!-- Todo List -->
<ul class="todo-list">
<template data-wp-each="state.filteredTodos">
<li
data-wp-class--completed="context.item.completed"
data-wp-class--editing="state.editingId === context.item.id"
>
<div class="view">
<!-- Toggle Completion -->
<input
type="checkbox"
data-wp-bind--checked="context.item.completed"
data-wp-on--change="actions.toggleTodo"
data-wp-bind--data-todo-id="context.item.id"
/>
<!-- Todo Title (double-click to edit) -->
<label
data-wp-text="context.item.title"
data-wp-on--dblclick="actions.startEditing"
data-wp-bind--data-todo-id="context.item.id"
></label>
<!-- Delete Button -->
<button
data-wp-on--click="actions.deleteTodo"
data-wp-bind--data-todo-id="context.item.id"
class="delete"
>
×
</button>
</div>
<!-- Edit Input (shown when editing) -->
<input
type="text"
data-wp-bind--value="state.editingTitle"
data-wp-on--input="actions.handleEditInput"
data-wp-on--blur="actions.finishEditing"
data-wp-on--keydown="actions.handleEditKeyDown"
class="edit-input"
/>
</li>
</template>
</ul>
<!-- Bulk Actions -->
<div class="bulk-actions">
<button
data-wp-on--click="actions.markAllComplete"
data-wp-bind--disabled="state.activeTodosCount === 0"
>
Mark All Complete
</button>
<button
data-wp-on--click="actions.clearCompleted"
data-wp-bind--disabled="state.completedTodosCount === 0"
>
Clear Completed
</button>
</div>
</div>// view.js
import { store, getElement, withSyncEvent } from '@wordpress/interactivity';
store( 'myPlugin', {
state: {
todos: [],
newTodoTitle: '',
filter: 'all',
editingId: null,
editingTitle: '',
// Derived state for filtering
get filteredTodos() {
switch ( this.filter ) {
case 'active':
return this.todos.filter( todo => ! todo.completed );
case 'completed':
return this.todos.filter( todo => todo.completed );
default:
return this.todos;
}
},
get activeTodosCount() {
return this.todos.filter( todo => ! todo.completed ).length;
},
get completedTodosCount() {
return this.todos.filter( todo => todo.completed ).length;
}
},
actions: {
// Add new todo
handleAddTodo: withSyncEvent( ( event ) => {
event.preventDefault();
const { state } = store( 'myPlugin' );
const title = state.newTodoTitle.trim();
if ( ! title ) {
return;
}
state.todos = [
...state.todos,
{
id: Date.now(),
title,
completed: false
}
];
state.newTodoTitle = '';
} ),
handleNewTodoInput( event ) {
const { state } = store( 'myPlugin' );
state.newTodoTitle = event.target.value;
},
// Handle Enter key in new todo input
handleNewTodoKeyDown: withSyncEvent( ( event ) => {
if ( event.key === 'Enter' ) {
event.preventDefault();
const { actions } = store( 'myPlugin' );
actions.handleAddTodo( event );
}
} ),
// Toggle todo completion
toggleTodo( event ) {
const { state } = store( 'myPlugin' );
const todoId = parseInt( event.target.dataset.todoId, 10 );
state.todos = state.todos.map( todo => {
if ( todo.id !== todoId ) {
return todo;
}
return { ...todo, completed: ! todo.completed };
} );
},
// Delete todo
deleteTodo( event ) {
const { state } = store( 'myPlugin' );
const todoId = parseInt( event.target.dataset.todoId, 10 );
state.todos = state.todos.filter( todo => todo.id !== todoId );
},
// Start editing (double-click)
startEditing( event ) {
const { state } = store( 'myPlugin' );
const todoId = parseInt( event.target.dataset.todoId, 10 );
const todo = state.todos.find( t => t.id === todoId );
if ( todo ) {
state.editingId = todoId;
state.editingTitle = todo.title;
// Focus input after render
setTimeout( () => {
const input = event.target.closest( 'li' ).querySelector( '.edit-input' );
if ( input ) {
input.focus();
}
}, 0 );
}
},
handleEditInput( event ) {
const { state } = store( 'myPlugin' );
state.editingTitle = event.target.value;
},
// Finish editing (blur or Enter)
finishEditing() {
const { state } = store( 'myPlugin' );
if ( state.editingId === null ) {
return;
}
const newTitle = state.editingTitle.trim();
if ( newTitle ) {
state.todos = state.todos.map( todo => {
if ( todo.id !== state.editingId ) {
return todo;
}
return { ...todo, title: newTitle };
} );
}
state.editingId = null;
state.editingTitle = '';
},
// Handle keyboard in edit mode
handleEditKeyDown: withSyncEvent( ( event ) => {
const { state, actions } = store( 'myPlugin' );
if ( event.key === 'Enter' ) {
event.preventDefault();
actions.finishEditing();
} else if ( event.key === 'Escape' ) {
event.preventDefault();
state.editingId = null;
state.editingTitle = '';
}
} ),
// Filter actions
setFilter( event ) {
const { state } = store( 'myPlugin' );
const filter = event.target.dataset.filter;
state.filter = filter;
},
// Bulk actions
markAllComplete() {
const { state } = store( 'myPlugin' );
state.todos = state.todos.map( todo => ( {
...todo,
completed: true
} ) );
},
clearCompleted() {
const { state } = store( 'myPlugin' );
state.todos = state.todos.filter( todo => ! todo.completed );
},
// Global keyboard shortcuts
handleGlobalKeyDown: withSyncEvent( ( event ) => {
const { actions } = store( 'myPlugin' );
// Ctrl/Cmd + K: Focus new todo input
if ( ( event.ctrlKey || event.metaKey ) && event.key === 'k' ) {
event.preventDefault();
const input = document.querySelector( '.new-todo-input' );
if ( input ) {
input.focus();
}
}
// Ctrl/Cmd + Shift + C: Clear completed
if ( ( event.ctrlKey || event.metaKey ) && event.shiftKey && event.key === 'C' ) {
event.preventDefault();
actions.clearCompleted();
}
} )
}
} );This implementation demonstrates:
- Form submission with validation
- Keyboard shortcuts (Enter to submit, Escape to cancel, Ctrl+K to focus)
- Inline editing with double-click activation
- Event delegation for dynamic list items
- Derived state for filtering and counts
- Immutable updates for all state modifications
- Accessibility through proper form elements and keyboard navigation
Common Pitfalls and Best Practices
Event Handler Naming Conventions
Use descriptive action names that indicate what happens, not just the event:
// ❌ Generic naming
actions: {
click() { /* ... */ },
input() { /* ... */ }
}
// ✅ Descriptive naming
actions: {
addItemToCart() { /* ... */ },
updateSearchQuery() { /* ... */ }
}When to Use withSyncEvent
Only use withSyncEvent() when you need synchronous event access:
// ✅ Use withSyncEvent for preventDefault/stopPropagation
handleFormSubmit: withSyncEvent( ( event ) => {
event.preventDefault();
// Handle submission
} )
// ✅ Use withSyncEvent for event properties needed immediately
handleKeyPress: withSyncEvent( ( event ) => {
if ( event.key === 'Enter' ) {
// Must check key synchronously
}
} )
// ❌ Don't use withSyncEvent unnecessarily
handleClick() {
// No event properties needed, regular action works fine
const { state } = store( 'myPlugin' );
state.count += 1;
}Memory Leaks and Clean up
The Interactivity API handles clean up automatically, but be careful with timers:
actions: {
startPolling() {
const { state } = store( 'myPlugin' );
// Store timer reference in state
state.pollingInterval = setInterval( () => {
// Polling logic
}, 5000 );
},
stopPolling() {
const { state } = store( 'myPlugin' );
if ( state.pollingInterval ) {
clearInterval( state.pollingInterval );
state.pollingInterval = null;
}
}
}Handling Rapid Events
For events that fire rapidly (scroll, mousemove, input), implement rate limiting:
// Use debouncing for user input
handleSearchInput( event ) {
const { state, actions } = store( 'myPlugin' );
state.searchQuery = event.target.value;
clearTimeout( state.searchTimeout );
state.searchTimeout = setTimeout( () => {
actions.performSearch();
}, 300 );
}
// Use throttling for scroll/resize
handleScroll( event ) {
const { state } = store( 'myPlugin' );
if ( state.scrollThrottled ) {
return;
}
state.scrollThrottled = true;
state.scrollPosition = event.target.scrollTop;
setTimeout( () => {
state.scrollThrottled = false;
}, 100 );
}Conclusion
The data-wp-on directive provides a declarative, maintainable approach to event handling in WordPress blocks. By connecting DOM events directly to store actions, you create reactive interfaces that respond naturally to user interactions, while keeping your code organized and testable.
Key Takeaways:
- Use
data-wp-on--[eventName]to connect events to actions - Access event objects, context, and DOM elements within actions
- Use
withSyncEvent()forpreventDefault(),stopPropagation(), and synchronous event properties - Implement debouncing for user input and throttling for continuous events
- Event delegation is automatic—no manual listener management needed
- Combine event handling with derived state for complex interactive features
