Advanced Patterns: Lifecycle, Side Effects, and Watchers

The WordPress Interactivity API provides powerful lifecycle management through directives like data-wp-initdata-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-init for 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-init for element initialization and third-party library setup
  • Implement clean up functions to prevent memory leaks
  • Use data-wp-watch for 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
Wp block editor book nobg

A comprehensive guide


Master WordPress Block Development

Everything you need to master blocks in one place, from the first block to enterprise architecture.

60-Day 100% Money-Back Guarantee. Zero risk.