Blog

L'Interactivity API de WordPress : l'état partagé entre blocs

Prenons une fonctionnalité que beaucoup de boutiques finissent par demander : un comparateur de produits. Sur la grille du catalogue, chaque fiche porte un bouton « Comparer ». En bas de l'écran, un bandeau reste collé et affiche les produits retenus, avec un petit « 2 / 3 ». Sur une page dédiée, un tableau croisé met les produits en colonnes et leurs caractéristiques en lignes.

Rien d'extraordinaire en apparence. Sauf que ces trois zones vivent à des endroits très éloignés du DOM, et qu'elles doivent rester d'accord en permanence. Je clique sur « Comparer » dans la grille : le bandeau doit se mettre à jour, le compteur aussi, et si j'arrive sur la page de comparaison, le tableau doit déjà connaître ma sélection.

C'est le cas d'école de l'état partagé. L'Interactivity API de WordPress le rend simple, là où les approches que j'utilisais avant demandaient beaucoup de plomberie. J'ai implémenté ce comparateur avec, je vais m'en servir comme fil rouge.

C'est le second volet d'une paire. Si vous débutez avec l'API, le premier article la prend par le plus simple, un bouton j'aime de bout en bout : WordPress sans jQuery, un bouton j'aime avec l'Interactivity API.


Le problème, posé clairement

Une page de catalogue, c'est facilement quinze à vingt boutons « Comparer » identiques. Plus un bandeau qui en est la synthèse. Quatre questions reviennent à chaque fois :

  • Où vit la vérité ? La liste des produits comparés ne peut pas être stockée dans un bouton en particulier.
  • Comment un clic ici met-il à jour un élément là-bas ?
  • Comment l'état survit-il à un changement de page ?
  • Comment éviter que le visiteur voie un état faux pendant une fraction de seconde au chargement ?

Gardez ces quatre questions en tête. Je vais y répondre avec deux outils que je n'utilise plus pour du neuf, puis avec celui que j'ai retenu.


Ce que ça donnait en jQuery

Pendant des années, sous WordPress, la réponse réflexe c'était jQuery. On accroche un gestionnaire de clic sur les boutons, on envoie la requête au serveur, et on prévient le reste de la page qu'un changement a eu lieu.

jQuery( '.compare-btn' ).on( 'click', function () {
    jQuery.post( ajaxurl, {
        action: 'compare_toggle',
        post_id: this.dataset.id
    }, function ( res ) {
        // prévenir le reste de la page qu'un produit a changé
        jQuery( document ).trigger( 'compare:updated', [ res.items ] );
    } );
} );

jQuery( document ).on( 'compare:updated', function ( e, items ) {
    redrawBar( items );      // reconstruire le bandeau à la main
    syncButtons( items );    // reparcourir tous les boutons pour réaligner leur état
} );

Ça marche. Je l'ai expédié en production plus d'une fois. Mais regardez ce que ça impose : la vérité est dispersée entre un événement custom, des fonctions qui redessinent le DOM, et l'état réel côté serveur. Chaque nouvelle vue qui dépend de la sélection, c'est une fonction redraw de plus à écrire et à ne jamais oublier d'appeler. Le jour où le bandeau et le tableau divergent d'un produit, vous passez l'après-midi à chercher quelle fonction de synchronisation a été zappée.


Et React, alors ? Il est déjà là

Objection légitime, surtout sous WordPress : React n'est pas un corps étranger ici. L'éditeur Gutenberg est lui-même écrit en React, exposé par le paquet @wordpress/element. Quand je développe un bloc, je suis déjà en React, côté éditeur. L'écarter d'un revers de main n'aurait pas de sens.

Mais l'éditeur, c'est le back-office. Côté visiteur, sur le front du site, React n'est pas chargé par défaut. La page du catalogue est rendue en PHP par WordPress, avec son SEO, ses images, son cache. Pour y greffer mon comparateur en React, il faudrait charger @wordpress/element sur le front, puis hydrater à la main les morceaux interactifs d'un HTML déjà produit côté serveur. Je redoublerais en JavaScript un balisage que le CMS sait déjà fabriquer, et j'hériterais d'une mécanique d'hydratation dont je me serais bien passé.

Pour quelques boutons et un bandeau, le rapport bénéfice/poids ne tient pas. J'en parle plus longuement dans mon article sur la sobriété numérique : monter une application côté client pour trois interactions, c'est le genre de réflexe « ça fait sérieux » qui coûte cher au client sans le servir.

C'est le créneau de l'Interactivity API : l'outil du front visiteur, là où React reste le socle de l'éditeur. On garde la réactivité d'un modèle déclaratif sans rejouer côté client une page déjà rendue par le serveur.


Le modèle de l'Interactivity API

L'API repose sur deux briques. D'un côté, un store : un objet JavaScript qui regroupe l'état (state), les actions et les valeurs calculées. De l'autre, des directives : des attributs HTML de la forme data-wp-* qui relient un élément du balisage à ce store.

Voici le cœur du store du comparateur. Le namespace identifie la fonctionnalité, l'état tient la liste des produits, et l'action parle au serveur.

import { store, getContext, getConfig } from '@wordpress/interactivity';

const { state } = store( 'product-compare', {
    state: {
        get isEmpty() {
            return state.items.length === 0;
        },
        get countLabel() {
            return `${ state.items.length } / ${ getConfig( 'product-compare' ).limit }`;
        },
    },
    actions: {
        *toggle() {
            const context = getContext();
            const { ajaxUrl } = getConfig( 'product-compare' );

            const res = yield fetch( ajaxUrl, {
                method: 'POST',
                body: new URLSearchParams( {
                    action: 'compare_toggle',
                    post_id: context.postId,
                } ),
            } );
            const data = yield res.json();

            state.items = data.items;   // une seule écriture, toutes les vues suivent
            context.added = data.added;
        },
    },
} );

Deux détails méritent qu'on s'y arrête. Les valeurs comme isEmpty ou countLabel sont des getters : dès que state.items change, elles se recalculent toutes seules. Et l'action est une fonction génératrice (le * et les yield) : l'API gère l'asynchrone à votre place, on écrit le flux comme s'il était synchrone, sans chaîne de promesses.


L'état partagé, en pratique

Tout se joue ici. Le bouton, dans la grille, déclare son comportement directement dans le HTML.

<button
    data-wp-interactive="product-compare"
    data-wp-context='{ "postId": 42, "added": false }'
    data-wp-on--click="actions.toggle"
    data-wp-class--is-added="context.added"
>
    Comparer
</button>

data-wp-on--click branche le clic sur l'action. data-wp-class--is-added ajoute ou retire une classe selon le contexte local du bouton, ce qui suffit à basculer son apparence une fois le produit retenu. data-wp-context porte les données propres à cette instance, ici l'identifiant du produit et son état. Pas une ligne de addEventListener, pas un sélecteur.

Maintenant le bandeau, qui se trouve tout en bas de la page, dans une zone complètement séparée du DOM :

<aside
    class="compare-bar"
    data-wp-interactive="product-compare"
    data-wp-bind--hidden="state.isEmpty"
>
    <p data-wp-text="state.countLabel"></p>

    <template data-wp-each--item="state.items">
        <span data-wp-text="context.item.title"></span>
        <button data-wp-on--click="actions.remove">Retirer</button>
    </template>
</aside>

Le bouton et le bandeau ne se connaissent pas. Ils ne partagent qu'une chose : le même store product-compare. Quand l'action toggle écrit state.items = data.items, c'est tout. Le bandeau se réaffiche, le compteur se recalcule via son getter, l'attribut hidden se met à jour parce que la liste n'est plus vide. Une seule écriture d'état, et chaque vue qui en dépend se réaligne.

Reprenez les fonctions redrawBar et syncButtons de la version jQuery. Elles ont disparu. C'est ça, la différence concrète : on ne décrit plus comment mettre à jour le DOM, on décrit à quoi chaque élément est lié, et le framework s'occupe du reste.


Le pont avec le serveur

Restent les deux dernières questions : comment l'état survit-il d'une page à l'autre, et comment éviter le clignotement au chargement ? La réponse tient dans deux fonctions PHP que WordPress expose côté serveur.

add_action( 'render_block', function ( $content, $block ) {
    if ( ! str_starts_with( $block['blockName'] ?? '', 'product-compare/' ) ) {
        return $content;
    }

    $items = get_compared_items(); // lus côté serveur : session, cookie, méta...

    wp_interactivity_state( 'product-compare', [
        'items' => $items,
    ] );

    wp_interactivity_config( 'product-compare', [
        'ajaxUrl' => admin_url( 'admin-ajax.php' ),
        'limit'   => 3,
    ] );

    return $content;
}, 10, 2 );

wp_interactivity_state() injecte l'état initial, celui qui change au fil des interactions. wp_interactivity_config() pose les valeurs fixes, comme l'URL d'appel ou la limite de produits. WordPress sérialise tout ça dans la page, et le store JavaScript le récupère au démarrage sans aucun appel réseau supplémentaire.

L'effet, c'est l'hydratation propre. Comme l'état vient du SSR, les boutons des produits déjà sélectionnés s'affichent d'emblée dans leur état « ajouté », le bandeau montre la bonne sélection dès le premier rendu. Pas de flash, pas de bandeau vide qui se remplit une demi-seconde plus tard. Le genre de détail que React rendrait pénible et que l'API offre par construction.


Les directives remplacent le DOM à la main

Le tableau croisé est l'endroit où l'approche déclarative paie le plus. Il faut itérer sur les caractéristiques en lignes, et pour chacune, sur les produits en colonnes. Deux boucles imbriquées, directement dans le balisage.

<table data-wp-interactive="product-compare">
    <template data-wp-each--field="state.fields">
        <tr>
            <th data-wp-text="context.field.label"></th>

            <template data-wp-each--item="state.items">
                <td data-wp-watch="callbacks.renderCell"></td>
            </template>
        </tr>
    </template>
</table>

En jQuery ou en JavaScript natif, ce tableau aurait demandé de tout reconstruire à la main à chaque changement de sélection : vider, recréer les lignes, recréer les cellules, réinsérer. Ici, data-wp-each s'en charge. Retirez un produit, la colonne correspondante s'efface. Le balisage décrit la forme, l'état décide du contenu.


Ce que l'API ne fait pas (encore) bien

Je ne vais pas vous la vendre comme parfaite, parce qu'elle ne l'est pas, et que les retours honnêtes sur le sujet sont rares en français.

Premier accroc, visible dans le tableau juste au-dessus : la cellule passe par data-wp-watch="callbacks.renderCell" et non par un simple data-wp-text. La raison : une caractéristique produit peut contenir du HTML (une icône, un lien), et l'API n'offre pas de directive native pour injecter du HTML riche en toute sécurité. On en est réduit à un contournement :

callbacks: {
    renderCell() {
        const { ref } = getElement();
        const context = getContext();

        // Pas de directive native pour du HTML riche : on passe par
        // innerHTML, en se reposant sur un contenu déjà assaini côté serveur.
        ref.innerHTML = context.item.values[ context.field.key ];
    },
},

Ça fonctionne, mais c'est un anti-pattern qu'il faut assumer : on contourne le modèle déclaratif, et on s'engage à ne jamais y faire passer de contenu non assaini. Acceptable quand la donnée vient du serveur, à proscrire avec une saisie utilisateur.

Les autres aspérités sont du même ordre : la documentation officielle reste jeune et avare d'exemples avancés, le débogage d'un état réactif demande un temps d'adaptation quand on vient du DOM manuel, et faire dialoguer deux stores différents (un store global et un store local à un bloc) marche très bien mais crée des dépendances qui ne sautent pas aux yeux à la lecture. Rien de rédhibitoire. Juste le prix d'une API encore en train de mûrir.


Quand l'utiliser, quand passer son tour

Mon critère est simple. Si la page est rendue par WordPress et que j'ai besoin d'y ajouter de l'interactivité (un filtre, un comparateur, un menu, un panier léger, un compteur), l'Interactivity API est le bon outil. Je reste dans l'écosystème Gutenberg, je garde le SSR et son SEO, je n'embarque pas de framework côté visiteur.

Si en revanche je construis une vraie interface très dynamique, un tableau de bord avec des dizaines d'états entremêlés, une application qui n'a plus grand-chose d'un site de contenu, alors React, déjà présent dans l'écosystème WordPress, garde tout son sens. L'Interactivity API ne cherche pas à remplacer ce terrain-là, elle s'occupe du front du visiteur.

Entre les deux, pour l'immense majorité des sites WordPress que je croise, le besoin réel penche du premier côté. On veut trois interactions propres, pas une SPA. Et sur ce terrain, avoir enfin un modèle réactif natif, hydraté depuis le serveur, ça change beaucoup par rapport au jQuery dispersé d'hier.

WordPress avance dans cette direction depuis quelques versions, et c'est une des raisons pour lesquelles il reste un choix solide en 2026 : j'en parle dans cet article sur WordPress face aux SaaS. L'Interactivity API en est une bonne illustration : le CMS gagne des outils modernes sans lâcher ce qui fait sa force, le rendu serveur.

Si vous avez une interaction de ce genre à intégrer et que vous hésitez sur l'approche, c'est le genre d'arbitrage dont je discute volontiers : prenez quelques minutes en visio, on regarde votre cas ensemble.

Questions fréquentes

FAQ

Qu'est-ce que l'Interactivity API de WordPress ?

C'est l'interface officielle de WordPress, stable depuis la version 6.5 (2024), pour rendre les blocs Gutenberg interactifs côté visiteur. On décrit le comportement directement dans le HTML, via des attributs data-wp-*, reliés à un état partagé appelé store. L'idée : obtenir une interface réactive (boutons, filtres, compteurs) sans charger un framework JavaScript complet comme React du côté du visiteur.

Faut-il connaître React pour utiliser l'Interactivity API ?

Non. L'API s'appuie en interne sur Preact et des signaux, mais on n'écrit pas de composants JSX. Le balisage reste du HTML classique avec des attributs spéciaux, et la logique tient dans un petit objet JavaScript (le store). Si vous savez écrire du HTML et un peu de JavaScript, vous pouvez démarrer. La courbe est nettement plus douce que celle d'un projet React complet.

L'Interactivity API remplace-t-elle jQuery ?

Pour la plupart des interactions courantes côté front, oui. Là où jQuery vous fait sélectionner des éléments puis redessiner le DOM à la main à chaque changement, l'Interactivity API maintient un état unique et met à jour les vues automatiquement. jQuery reste présent dans beaucoup de thèmes et plugins existants, mais pour du neuf, l'API native est un meilleur point de départ.

Quand utiliser l'Interactivity API plutôt que React ?

Quand votre page est rendue par WordPress côté serveur et que vous voulez y greffer de l'interactivité (filtres, comparateur, menu, panier léger) sans basculer dans une application JavaScript. React reste pertinent pour une vraie interface très dynamique, type tableau de bord ou application monopage. Pour un site de contenu ou une boutique classique, l'Interactivity API évite d'embarquer un framework entier pour quelques interactions.

L'Interactivity API fonctionne-t-elle avec un thème WordPress classique ?

Elle est pensée pour les blocs Gutenberg, donc le terrain naturel est un thème de blocs (Full Site Editing). Mais elle n'exige pas le FSE : tant que votre interactivité passe par des blocs ou par du markup auquel vous attachez les directives data-wp-*, elle fonctionne aussi dans un thème classique qui utilise l'éditeur de blocs. Le vrai prérequis, c'est WordPress 6.5 ou plus récent.

Essayer Gutenberg en direct

Lancez un WordPress complet dans votre navigateur, sans installation

Avant d'engager quoi que ce soit, vous pouvez ouvrir un WordPress de démonstration dans votre navigateur grâce au projet WordPress Playground, sans compte ni installation. Quelques minutes suffisent pour vous faire une idée concrète de l'éditeur Gutenberg et du Full Site Editing, et comparer avec ce que vous connaissez d'Elementor ou Divi.

Tester Gutenberg maintenant
Parlons-en

Discutons de votre projet

Décrivez votre besoin en quelques lignes : je vous réponds sous 24 à 48 heures, sans engagement.

Champs marqués d'un obligatoires. Vos données servent uniquement à traiter votre demande.