The WordPress Interactivity API provides powerful lifecycle management through directives like data-wp-init, data-wp-watch, and callback patterns that enable sophisticated behaviours: initializing third-party libraries, managing side effects, persisting data, integrating analytics, and reacting to state changes. These advanced patterns unlock the full potential of the Interactivity API for building complex, production-ready WordPress applications.
In this final article of our series, we’ll explore lifecycle directives, side effect orchestration, state watchers, external library integration, and architectural patterns for large-scale applications.
Understanding Lifecycle and Initialization
Lifecycle directives control when code executes in relation to element rendering and state changes.
data-wp-init: Element Initialization
The data-wp-init directive runs a callback once when an element is first rendered, perfect for initializing third-party libraries or setting up subscriptions.
// view.js
import { store } from '@wordpress/interactivity';
store( 'myPlugin', {
callbacks: {
initializeGallery() {
const { ref } = getElement();
// Initialize third-party gallery library
if ( typeof Lightbox !== 'undefined' ) {
new Lightbox( ref );
}
},
setupVideoPlayer() {
const { ref } = getElement();
// Initialize video player
if ( ref && typeof Plyr !== 'undefined' ) {
const player = new Plyr( ref, {
controls: [ 'play', 'progress', 'current-time', 'mute', 'volume', 'fullscreen' ]
} );
// Store player instance for cleanup
ref.playerInstance = player;
}
},
observeIntersection() {
const { ref } = getElement();
const { state, actions } = store( 'myPlugin' );
// Set up Intersection Observer for lazy loading
const observer = new IntersectionObserver( ( entries ) => {
entries.forEach( entry => {
if ( entry.isIntersecting ) {
actions.loadContent();
observer.disconnect();
}
} );
} );
observer.observe( ref );
// Store observer for cleanup
ref.intersectionObserver = observer;
}
}
} );<div data-wp-interactive="myPlugin">
<!-- Initialize gallery on render -->
<div
class="gallery"
data-wp-init="callbacks.initializeGallery"
>
<img src="image1.jpg" alt="Image 1" />
<img src="image2.jpg" alt="Image 2" />
<img src="image3.jpg" alt="Image 3" />
</div>
<!-- Initialize video player -->
<video
data-wp-init="callbacks.setupVideoPlayer"
src="video.mp4"
></video>
<!-- Lazy load content when visible -->
<div
data-wp-init="callbacks.observeIntersection"
class="lazy-content"
>
<span data-wp-text="state.content"></span>
</div>
</div>Important: data-wp-init runs only once per element. If the element is removed and re-rendered (e.g., within a data-wp-each loop), the callback runs again for the new instance.
Clean up Patterns
Always clean up resources when elements are removed:
store( 'myPlugin', {
callbacks: {
initializeChart() {
const { ref } = getElement();
const { state } = store( 'myPlugin' );
// Initialize Chart.js
const ctx = ref.getContext( '2d' );
const chart = new Chart( ctx, {
type: 'line',
data: {
labels: state.chartLabels,
datasets: [ {
label: 'Sales',
data: state.chartData
} ]
}
} );
// Store instance for updates and cleanup
ref.chartInstance = chart;
// Set up cleanup
return () => {
if ( ref.chartInstance ) {
ref.chartInstance.destroy();
ref.chartInstance = null;
}
};
},
setupWebSocket() {
const { state } = store( 'myPlugin' );
// Create WebSocket connection
const ws = new WebSocket( 'wss://example.com/updates' );
ws.onmessage = ( event ) => {
const data = JSON.parse( event.data );
state.liveData = data;
};
state.websocket = ws;
// Cleanup function
return () => {
if ( state.websocket ) {
state.websocket.close();
state.websocket = null;
}
};
}
}
} );Clean up Pattern: Return a function from your data-wp-init callback. This function will be called automatically when the element is removed from the DOM.
data-wp-watch: Reactive Side Effects
The data-wp-watch directive runs a callback whenever its dependencies (state properties) change, enabling reactive side effects.
Basic Watchers
store( 'myPlugin', {
state: {
searchQuery: '',
selectedCategory: 'all',
darkMode: false,
volume: 50
},
callbacks: {
// Watch search query changes
watchSearch() {
const { state, actions } = store( 'myPlugin' );
// This runs whenever state.searchQuery changes
console.log( 'Search query changed to:', state.searchQuery );
// Debounce API call
clearTimeout( state.searchTimeout );
state.searchTimeout = setTimeout( () => {
actions.performSearch();
}, 300 );
},
// Watch theme changes
watchTheme() {
const { state } = store( 'myPlugin' );
// Update document class
document.documentElement.classList.toggle( 'dark-mode', state.darkMode );
// Persist to localStorage
localStorage.setItem( 'darkMode', state.darkMode );
},
// Watch volume changes
watchVolume() {
const { state } = store( 'myPlugin' );
const { ref } = getElement();
// Update video/audio element volume
if ( ref && ref.volume !== undefined ) {
ref.volume = state.volume / 100;
}
}
}
} );<div data-wp-interactive="myPlugin">
<!-- Watch search query -->
<input
type="search"
data-wp-bind--value="state.searchQuery"
data-wp-on--input="actions.updateSearchQuery"
data-wp-watch="callbacks.watchSearch"
/>
<!-- Watch dark mode toggle -->
<label data-wp-watch="callbacks.watchTheme">
<input
type="checkbox"
data-wp-bind--checked="state.darkMode"
data-wp-on--change="actions.toggleDarkMode"
/>
Dark Mode
</label>
<!-- Watch volume -->
<video
src="video.mp4"
data-wp-watch="callbacks.watchVolume"
></video>
<input
type="range"
min="0"
max="100"
data-wp-bind--value="state.volume"
data-wp-on--input="actions.updateVolume"
/>
</div>Multiple Dependencies
Watchers automatically track which state properties they access:
store( 'myPlugin', {
state: {
filters: {
category: 'all',
priceMin: 0,
priceMax: 1000,
inStock: false
},
sortBy: 'name'
},
callbacks: {
// Watches multiple filter properties
watchFilters() {
const { state, actions } = store( 'myPlugin' );
// Accesses multiple state properties
const { category, priceMin, priceMax, inStock } = state.filters;
const { sortBy } = state;
console.log( 'Filters changed:', {
category,
priceMin,
priceMax,
inStock,
sortBy
} );
// Any change to these properties triggers this watcher
actions.fetchFilteredProducts();
}
}
} );<div
data-wp-interactive="myPlugin"
data-wp-watch="callbacks.watchFilters"
>
<select
data-wp-bind--value="state.filters.category"
data-wp-on--change="actions.updateCategory"
>
<option value="all">All Categories</option>
<option value="electronics">Electronics</option>
<option value="clothing">Clothing</option>
</select>
<input
type="range"
data-wp-bind--value="state.filters.priceMin"
data-wp-on--input="actions.updatePriceMin"
/>
<label>
<input
type="checkbox"
data-wp-bind--checked="state.filters.inStock"
data-wp-on--change="actions.toggleInStock"
/>
In Stock Only
</label>
</div>Integrating External Libraries
Chart Libraries (Chart.js)
<?php
// render.php
wp_enqueue_script( 'chartjs', 'https://cdn.jsdelivr.net/npm/chart.js', array(), '4.0.0', true );
wp_interactivity_state( 'myPlugin', array(
'chartData' => array(
'labels' => array( 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun' ),
'datasets' => array(
array(
'label' => 'Sales',
'data' => array( 120, 150, 180, 200, 170, 190 )
)
)
),
'chartType' => 'line'
) );
?>
<div data-wp-interactive="myPlugin">
<!-- Chart type selector -->
<select
data-wp-bind--value="state.chartType"
data-wp-on--change="actions.updateChartType"
>
<option value="line">Line</option>
<option value="bar">Bar</option>
<option value="pie">Pie</option>
</select>
<!-- Chart canvas -->
<canvas
data-wp-init="callbacks.initializeChart"
data-wp-watch="callbacks.updateChart"
></canvas>
</div>import { store, getElement } from '@wordpress/interactivity';
store( 'myPlugin', {
callbacks: {
initializeChart() {
const { ref } = getElement();
const { state } = store( 'myPlugin' );
if ( typeof Chart === 'undefined' ) {
console.error( 'Chart.js not loaded' );
return;
}
const ctx = ref.getContext( '2d' );
const chart = new Chart( ctx, {
type: state.chartType,
data: state.chartData,
options: {
responsive: true,
maintainAspectRatio: false
}
} );
// Store instance
ref.chartInstance = chart;
// Cleanup
return () => {
if ( ref.chartInstance ) {
ref.chartInstance.destroy();
}
};
},
updateChart() {
const { ref } = getElement();
const { state } = store( 'myPlugin' );
if ( ! ref.chartInstance ) {
return;
}
// Update chart type
ref.chartInstance.config.type = state.chartType;
// Update data
ref.chartInstance.data = state.chartData;
// Re-render
ref.chartInstance.update();
}
},
actions: {
updateChartType( event ) {
const { state } = store( 'myPlugin' );
state.chartType = event.target.value;
}
}
} );Map Libraries (Leaflet)
store( 'myPlugin', {
state: {
locations: [
{ lat: 51.505, lng: -0.09, title: 'Location 1' },
{ lat: 51.515, lng: -0.1, title: 'Location 2' }
],
mapCenter: [ 51.505, -0.09 ],
mapZoom: 13
},
callbacks: {
initializeMap() {
const { ref } = getElement();
const { state } = store( 'myPlugin' );
if ( typeof L === 'undefined' ) {
console.error( 'Leaflet not loaded' );
return;
}
// Create map
const map = L.map( ref ).setView( state.mapCenter, state.mapZoom );
// Add tile layer
L.tileLayer( 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
} ).addTo( map );
// Add markers
state.locations.forEach( location => {
L.marker( [ location.lat, location.lng ] )
.addTo( map )
.bindPopup( location.title );
} );
// Store instance
ref.mapInstance = map;
// Cleanup
return () => {
if ( ref.mapInstance ) {
ref.mapInstance.remove();
}
};
},
updateMarkers() {
const { ref } = getElement();
const { state } = store( 'myPlugin' );
if ( ! ref.mapInstance ) {
return;
}
// Clear existing markers
ref.mapInstance.eachLayer( layer => {
if ( layer instanceof L.Marker ) {
layer.remove();
}
} );
// Add new markers
state.locations.forEach( location => {
L.marker( [ location.lat, location.lng ] )
.addTo( ref.mapInstance )
.bindPopup( location.title );
} );
}
}
} );<div data-wp-interactive="myPlugin">
<div
id="map"
data-wp-init="callbacks.initializeMap"
data-wp-watch="callbacks.updateMarkers"
style="height: 400px;"
></div>
</div>Analytics Integration
Track user interactions and page views with proper event handling:
store( 'myPlugin', {
state: {
currentPage: window.location.pathname,
sessionId: null,
trackingEnabled: true
},
callbacks: {
initializeAnalytics() {
const { state, actions } = store( 'myPlugin' );
// Generate session ID
state.sessionId = 'session_' + Date.now() + '_' + Math.random().toString( 36 ).substr( 2, 9 );
// Track initial page view
actions.trackPageView( window.location.pathname );
// Load consent from localStorage
const consent = localStorage.getItem( 'analyticsConsent' );
state.trackingEnabled = consent === 'accepted';
},
watchNavigation() {
const { state, actions } = store( 'myPlugin' );
// Track page views on navigation
if ( state.currentPage !== window.location.pathname ) {
state.currentPage = window.location.pathname;
actions.trackPageView( state.currentPage );
}
}
},
actions: {
trackPageView( path ) {
const { state } = store( 'myPlugin' );
if ( ! state.trackingEnabled ) {
return;
}
// Send to analytics endpoint
fetch( '/wp-json/myplugin/v1/analytics/pageview', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify( {
path,
sessionId: state.sessionId,
timestamp: new Date().toISOString(),
referrer: document.referrer
} )
} ).catch( error => {
console.error( 'Analytics tracking failed:', error );
} );
// Also send to Google Analytics if available
if ( typeof gtag !== 'undefined' ) {
gtag( 'config', 'GA_MEASUREMENT_ID', {
page_path: path
} );
}
},
trackEvent( eventName, eventData ) {
const { state } = store( 'myPlugin' );
if ( ! state.trackingEnabled ) {
return;
}
// Send to analytics endpoint
fetch( '/wp-json/myplugin/v1/analytics/event', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify( {
event: eventName,
data: eventData,
sessionId: state.sessionId,
timestamp: new Date().toISOString(),
page: window.location.pathname
} )
} ).catch( error => {
console.error( 'Event tracking failed:', error );
} );
},
// Track specific interactions
trackButtonClick( buttonName ) {
const { actions } = store( 'myPlugin' );
actions.trackEvent( 'button_click', { button: buttonName } );
},
trackFormSubmission( formName ) {
const { actions } = store( 'myPlugin' );
actions.trackEvent( 'form_submission', { form: formName } );
},
acceptAnalytics() {
const { state } = store( 'myPlugin' );
state.trackingEnabled = true;
localStorage.setItem( 'analyticsConsent', 'accepted' );
}
}
} );<div
data-wp-interactive="myPlugin"
data-wp-init="callbacks.initializeAnalytics"
data-wp-watch="callbacks.watchNavigation"
>
<!-- Tracked button -->
<button
data-wp-on--click="actions.trackButtonClick"
data-wp-bind--data-button-name="'cta-hero'"
>
Get Started
</button>
<!-- Tracked form -->
<form
data-wp-on--submit="actions.trackFormSubmission"
data-wp-bind--data-form-name="'contact'"
>
<!-- form fields -->
</form>
<!-- Analytics consent banner -->
<div data-wp-class--hidden="state.trackingEnabled">
<p>We use analytics to improve your experience.</p>
<button data-wp-on--click="actions.acceptAnalytics">
Accept
</button>
</div>
</div>Data Persistence with localStorage
Persist and restore state across sessions:
store( 'myPlugin', {
state: {
preferences: {
theme: 'light',
language: 'en',
fontSize: 16,
notifications: true
},
cart: [],
recentlyViewed: []
},
callbacks: {
// Load persisted state on initialization
loadPersistedState() {
const { state } = store( 'myPlugin' );
try {
// Load preferences
const savedPreferences = localStorage.getItem( 'userPreferences' );
if ( savedPreferences ) {
state.preferences = JSON.parse( savedPreferences );
}
// Load cart
const savedCart = localStorage.getItem( 'cart' );
if ( savedCart ) {
state.cart = JSON.parse( savedCart );
}
// Load recently viewed
const savedRecentlyViewed = localStorage.getItem( 'recentlyViewed' );
if ( savedRecentlyViewed ) {
state.recentlyViewed = JSON.parse( savedRecentlyViewed );
}
} catch ( error ) {
console.error( 'Failed to load persisted state:', error );
}
},
// Watch and persist preferences
watchPreferences() {
const { state } = store( 'myPlugin' );
try {
localStorage.setItem(
'userPreferences',
JSON.stringify( state.preferences )
);
} catch ( error ) {
console.error( 'Failed to persist preferences:', error );
}
},
// Watch and persist cart
watchCart() {
const { state } = store( 'myPlugin' );
try {
localStorage.setItem(
'cart',
JSON.stringify( state.cart )
);
} catch ( error ) {
console.error( 'Failed to persist cart:', error );
}
},
// Watch and persist recently viewed
watchRecentlyViewed() {
const { state } = store( 'myPlugin' );
try {
// Limit to last 10 items
const limited = state.recentlyViewed.slice( -10 );
localStorage.setItem(
'recentlyViewed',
JSON.stringify( limited )
);
} catch ( error ) {
console.error( 'Failed to persist recently viewed:', error );
}
}
},
actions: {
updateTheme( theme ) {
const { state } = store( 'myPlugin' );
state.preferences = {
...state.preferences,
theme
};
},
addToCart( product ) {
const { state } = store( 'myPlugin' );
// Check if product already in cart
const existingIndex = state.cart.findIndex( item => item.id === product.id );
if ( existingIndex >= 0 ) {
// Update quantity
state.cart = state.cart.map( ( item, index ) => {
if ( index === existingIndex ) {
return { ...item, quantity: item.quantity + 1 };
}
return item;
} );
} else {
// Add new item
state.cart = [ ...state.cart, { ...product, quantity: 1 } ];
}
},
addToRecentlyViewed( productId ) {
const { state } = store( 'myPlugin' );
// Remove if already exists (to move to end)
const filtered = state.recentlyViewed.filter( id => id !== productId );
// Add to end
state.recentlyViewed = [ ...filtered, productId ];
}
}
} );<div
data-wp-interactive="myPlugin"
data-wp-init="callbacks.loadPersistedState"
>
<!-- Watch preferences -->
<div data-wp-watch="callbacks.watchPreferences">
<select
data-wp-bind--value="state.preferences.theme"
data-wp-on--change="actions.updateTheme"
>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</div>
<!-- Watch cart -->
<div data-wp-watch="callbacks.watchCart">
<p>Cart items: <span data-wp-text="state.cart.length"></span></p>
</div>
<!-- Watch recently viewed -->
<div data-wp-watch="callbacks.watchRecentlyViewed">
<h3>Recently Viewed</h3>
<template data-wp-each="state.recentlyViewed">
<div data-wp-text="context.item"></div>
</template>
</div>
</div>Real-Time Updates with WebSockets
Implement live data updates with WebSocket connections:
store( 'myPlugin', {
state: {
connected: false,
notifications: [],
liveStats: {
viewers: 0,
likes: 0,
comments: 0
}
},
callbacks: {
initializeWebSocket() {
const { state, actions } = store( 'myPlugin' );
// Create WebSocket connection
const ws = new WebSocket( 'wss://example.com/live' );
ws.onopen = () => {
state.connected = true;
console.log( 'WebSocket connected' );
};
ws.onmessage = ( event ) => {
const data = JSON.parse( event.data );
// Handle different message types
switch ( data.type ) {
case 'notification':
actions.addNotification( data.payload );
break;
case 'stats_update':
state.liveStats = data.payload;
break;
case 'user_joined':
state.liveStats.viewers += 1;
break;
case 'user_left':
state.liveStats.viewers -= 1;
break;
}
};
ws.onerror = ( error ) => {
console.error( 'WebSocket error:', error );
state.connected = false;
};
ws.onclose = () => {
state.connected = false;
console.log( 'WebSocket disconnected' );
// Attempt reconnection after delay
setTimeout( () => {
actions.reconnectWebSocket();
}, 5000 );
};
// Store WebSocket instance
state.websocket = ws;
// Cleanup
return () => {
if ( state.websocket ) {
state.websocket.close();
state.websocket = null;
}
};
}
},
actions: {
addNotification( notification ) {
const { state } = store( 'myPlugin' );
state.notifications = [
...state.notifications,
{
...notification,
id: Date.now(),
timestamp: new Date().toISOString()
}
];
// Remove after 5 seconds
setTimeout( () => {
state.notifications = state.notifications.filter(
n => n.id !== notification.id
);
}, 5000 );
},
sendMessage( message ) {
const { state } = store( 'myPlugin' );
if ( state.connected && state.websocket ) {
state.websocket.send( JSON.stringify( {
type: 'message',
payload: message
} ) );
}
},
reconnectWebSocket() {
const { callbacks } = store( 'myPlugin' );
callbacks.initializeWebSocket();
}
}
} );<div
data-wp-interactive="myPlugin"
data-wp-init="callbacks.initializeWebSocket"
>
<!-- Connection status -->
<div
data-wp-class--connected="state.connected"
data-wp-class--disconnected="!state.connected"
class="status-indicator"
>
<span data-wp-text="state.connected ? 'Connected' : 'Disconnected'"></span>
</div>
<!-- Live statistics -->
<div class="live-stats">
<div>
<span data-wp-text="state.liveStats.viewers"></span> viewers
</div>
<div>
<span data-wp-text="state.liveStats.likes"></span> likes
</div>
<div>
<span data-wp-text="state.liveStats.comments"></span> comments
</div>
</div>
<!-- Notifications -->
<div class="notifications">
<template data-wp-each="state.notifications">
<div class="notification">
<span data-wp-text="context.item.message"></span>
</div>
</template>
</div>
</div>Real-World Implementation: Advanced Product Viewer
Complete example combining lifecycle, side effects, external libraries, and persistence:
<?php
// render.php
wp_enqueue_script( 'three', 'https://cdn.jsdelivr.net/npm/three@0.150.0/build/three.min.js', array(), null, true );
wp_enqueue_script( 'chartjs', 'https://cdn.jsdelivr.net/npm/chart.js', array(), '4.0.0', true );
$product_id = get_the_ID();
$product_data = array(
'id' => $product_id,
'name' => get_the_title(),
'price' => get_post_meta( $product_id, 'price', true ),
'images' => get_post_meta( $product_id, 'images', true ),
'model3d' => get_post_meta( $product_id, 'model_3d_url', true ),
'reviews' => get_comments( array( 'post_id' => $product_id ) ),
);
wp_interactivity_state( 'productViewer', array(
'product' => $product_data,
'view' => '2d', // 2d, 3d, ar
'selectedImage' => 0,
'zoom' => 1,
'rotation' => 0,
'showReviews' => true,
'userPreferences' => array(
'autoRotate' => true,
'showAnnotations' => true
)
) );
?>
<div
data-wp-interactive="productViewer"
class="product-viewer"
data-wp-init="callbacks.initializeViewer"
data-wp-watch="callbacks.trackProductView"
>
<!-- View mode selector -->
<div class="view-selector">
<button
data-wp-on--click="actions.setView"
data-wp-bind--data-view="'2d'"
data-wp-class--active="state.view === '2d'"
>
2D Gallery
</button>
<button
data-wp-on--click="actions.setView"
data-wp-bind--data-view="'3d'"
data-wp-class--active="state.view === '3d'"
>
3D Model
</button>
<button
data-wp-on--click="actions.setView"
data-wp-bind--data-view="'ar'"
data-wp-class--active="state.view === 'ar'"
data-wp-bind--disabled="!state.arSupported"
>
View in AR
</button>
</div>
<!-- 2D Image Gallery -->
<div
data-wp-class--visible="state.view === '2d'"
data-wp-class--hidden="state.view !== '2d'"
class="gallery-2d"
>
<!-- Main image -->
<div
class="main-image"
data-wp-init="callbacks.initializeLightbox"
>
<img
data-wp-bind--src="state.product.images[state.selectedImage]"
data-wp-bind--alt="state.product.name"
data-wp-style--transform="'scale(' + state.zoom + ') rotate(' + state.rotation + 'deg)'"
/>
</div>
<!-- Thumbnails -->
<div class="thumbnails">
<template data-wp-each="state.product.images">
<img
data-wp-bind--src="context.item"
data-wp-on--click="actions.selectImage"
data-wp-bind--data-index="context.index"
data-wp-class--active="state.selectedImage === context.index"
/>
</template>
</div>
<!-- Controls -->
<div class="image-controls">
<button data-wp-on--click="actions.zoomIn">+</button>
<button data-wp-on--click="actions.zoomOut">-</button>
<button data-wp-on--click="actions.rotateLeft">↺</button>
<button data-wp-on--click="actions.rotateRight">↻</button>
</div>
</div>
<!-- 3D Model Viewer -->
<div
data-wp-class--visible="state.view === '3d'"
data-wp-class--hidden="state.view !== '3d'"
class="viewer-3d"
>
<canvas
data-wp-init="callbacks.initialize3DViewer"
data-wp-watch="callbacks.update3DViewer"
></canvas>
<div class="model-controls">
<label>
<input
type="checkbox"
data-wp-bind--checked="state.userPreferences.autoRotate"
data-wp-on--change="actions.toggleAutoRotate"
/>
Auto Rotate
</label>
<label>
<input
type="checkbox"
data-wp-bind--checked="state.userPreferences.showAnnotations"
data-wp-on--change="actions.toggleAnnotations"
/>
Show Annotations
</label>
</div>
</div>
<!-- Product Info -->
<div class="product-info">
<h1 data-wp-text="state.product.name"></h1>
<p class="price" data-wp-text="'$' + state.product.price"></p>
<button
data-wp-on--click="actions.addToCart"
class="add-to-cart"
>
Add to Cart
</button>
</div>
<!-- Reviews Chart -->
<div
data-wp-class--visible="state.showReviews"
class="reviews-section"
>
<h2>Customer Reviews</h2>
<canvas
data-wp-init="callbacks.initializeReviewsChart"
data-wp-watch="callbacks.updateReviewsChart"
></canvas>
</div>
</div>// view.js
import { store, getElement } from '@wordpress/interactivity';
store( 'productViewer', {
callbacks: {
// Initialize on page load
initializeViewer() {
const { state, actions } = store( 'productViewer' );
// Load user preferences
actions.loadPreferences();
// Track product view
actions.trackProductView();
// Check AR support
state.arSupported = navigator.xr !== undefined;
},
// Initialize lightbox for 2D gallery
initializeLightbox() {
const { ref } = getElement();
if ( typeof Lightbox !== 'undefined' ) {
new Lightbox( ref );
}
return () => {
// Cleanup lightbox
};
},
// Initialize 3D viewer
initialize3DViewer() {
const { ref } = getElement();
const { state } = store( 'productViewer' );
if ( typeof THREE === 'undefined' ) {
console.error( 'Three.js not loaded' );
return;
}
// Create scene
const scene = new THREE.Scene();
scene.background = new THREE.Color( 0xf0f0f0 );
// Create camera
const camera = new THREE.PerspectiveCamera(
75,
ref.clientWidth / ref.clientHeight,
0.1,
1000
);
camera.position.z = 5;
// Create renderer
const renderer = new THREE.WebGLRenderer( {
canvas: ref,
antialias: true
} );
renderer.setSize( ref.clientWidth, ref.clientHeight );
// Add lights
const ambientLight = new THREE.AmbientLight( 0x404040 );
scene.add( ambientLight );
const directionalLight = new THREE.DirectionalLight( 0xffffff, 0.5 );
directionalLight.position.set( 1, 1, 1 );
scene.add( directionalLight );
// Load 3D model
const loader = new THREE.GLTFLoader();
loader.load(
state.product.model3d,
( gltf ) => {
scene.add( gltf.scene );
ref.model = gltf.scene;
},
undefined,
( error ) => {
console.error( 'Error loading 3D model:', error );
}
);
// Animation loop
const animate = () => {
requestAnimationFrame( animate );
if ( state.userPreferences.autoRotate && ref.model ) {
ref.model.rotation.y += 0.01;
}
renderer.render( scene, camera );
};
animate();
// Store instances
ref.scene = scene;
ref.camera = camera;
ref.renderer = renderer;
// Handle window resize
const handleResize = () => {
camera.aspect = ref.clientWidth / ref.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize( ref.clientWidth, ref.clientHeight );
};
window.addEventListener( 'resize', handleResize );
// Cleanup
return () => {
window.removeEventListener( 'resize', handleResize );
if ( ref.renderer ) {
ref.renderer.dispose();
}
};
},
update3DViewer() {
// Handle preferences updates
const { state } = store( 'productViewer' );
const { ref } = getElement();
if ( ref.model && state.userPreferences.showAnnotations ) {
// Show/hide annotations
}
},
// Initialize reviews chart
initializeReviewsChart() {
const { ref } = getElement();
const { state } = store( 'productViewer' );
if ( typeof Chart === 'undefined' ) {
return;
}
// Process review data
const ratings = { 5: 0, 4: 0, 3: 0, 2: 0, 1: 0 };
state.product.reviews.forEach( review => {
ratings[ review.rating ] += 1;
} );
const ctx = ref.getContext( '2d' );
const chart = new Chart( ctx, {
type: 'bar',
data: {
labels: [ '5 Stars', '4 Stars', '3 Stars', '2 Stars', '1 Star' ],
datasets: [ {
label: 'Number of Reviews',
data: [ ratings[ 5 ], ratings[ 4 ], ratings[ 3 ], ratings[ 2 ], ratings[ 1 ] ],
backgroundColor: 'rgba(54, 162, 235, 0.5)'
} ]
},
options: {
responsive: true,
maintainAspectRatio: false
}
} );
ref.chartInstance = chart;
return () => {
if ( ref.chartInstance ) {
ref.chartInstance.destroy();
}
};
},
updateReviewsChart() {
// Update chart if review data changes
},
// Track product view
trackProductView() {
const { state } = store( 'productViewer' );
// Add to recently viewed
const recentlyViewed = JSON.parse(
localStorage.getItem( 'recentlyViewed' ) || '[]'
);
const filtered = recentlyViewed.filter( id => id !== state.product.id );
const updated = [ ...filtered, state.product.id ].slice( -10 );
localStorage.setItem( 'recentlyViewed', JSON.stringify( updated ) );
// Send analytics
fetch( '/wp-json/analytics/v1/product-view', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify( {
productId: state.product.id,
view: state.view,
timestamp: new Date().toISOString()
} )
} );
}
},
actions: {
setView( event ) {
const { state } = store( 'productViewer' );
const view = event.target.dataset.view;
state.view = view;
},
selectImage( event ) {
const { state } = store( 'productViewer' );
state.selectedImage = parseInt( event.target.dataset.index, 10 );
},
zoomIn() {
const { state } = store( 'productViewer' );
state.zoom = Math.min( state.zoom + 0.2, 3 );
},
zoomOut() {
const { state } = store( 'productViewer' );
state.zoom = Math.max( state.zoom - 0.2, 0.5 );
},
rotateLeft() {
const { state } = store( 'productViewer' );
state.rotation = ( state.rotation - 90 ) % 360;
},
rotateRight() {
const { state } = store( 'productViewer' );
state.rotation = ( state.rotation + 90 ) % 360;
},
toggleAutoRotate( event ) {
const { state, actions } = store( 'productViewer' );
state.userPreferences.autoRotate = event.target.checked;
actions.savePreferences();
},
toggleAnnotations( event ) {
const { state, actions } = store( 'productViewer' );
state.userPreferences.showAnnotations = event.target.checked;
actions.savePreferences();
},
loadPreferences() {
const { state } = store( 'productViewer' );
const saved = localStorage.getItem( 'viewerPreferences' );
if ( saved ) {
state.userPreferences = JSON.parse( saved );
}
},
savePreferences() {
const { state } = store( 'productViewer' );
localStorage.setItem(
'viewerPreferences',
JSON.stringify( state.userPreferences )
);
},
addToCart() {
const { state } = store( 'productViewer' );
fetch( '/wp-json/wc/v3/cart/add', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify( {
product_id: state.product.id,
quantity: 1
} )
} )
.then( response => response.json() )
.then( data => {
console.log( 'Added to cart:', data );
} )
.catch( error => {
console.error( 'Failed to add to cart:', error );
} );
},
trackProductView() {
const { state } = store( 'productViewer' );
fetch( '/wp-json/analytics/v1/product-view', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify( {
productId: state.product.id,
timestamp: new Date().toISOString()
} )
} );
}
}
} );This advanced product viewer demonstrates:
- Lifecycle management with
data-wp-initfor libraries - Multiple view modes (2D gallery, 3D model, AR)
- Third-party library integration (Three.js for 3D, Chart.js for reviews)
- Data persistence with localStorage for preferences
- Analytics tracking for product views
- Clean up patterns for proper resource management
- Responsive interactions combining all previously covered patterns
Conclusion
Advanced patterns like lifecycle management, side effects, watchers, and external library integration unlock the full potential of the WordPress Interactivity API. By combining these techniques with the state management and directive patterns from earlier articles, you can build sophisticated, production-ready interactive WordPress applications.
Key Takeaways:
- Use
data-wp-initfor element initialization and third-party library setup - Implement clean up functions to prevent memory leaks
- Use
data-wp-watchfor reactive side effects based on state changes - Integrate external libraries (Chart.js, Leaflet, Three.js) with proper lifecycle management
- Track user interactions with analytics integration
- Persist state across sessions with localStorage
- Implement real-time features with WebSocket connections
- Combine all patterns for complex, real-world applications
