Blog

WordPress sans jQuery : un bouton j'aime avec l'Interactivity API

Sur un site de contenu récent, j'ai eu besoin d'une petite chose toute bête : un bouton « j'aime » sous chaque article. On clique, le cœur se remplit, le compteur monte d'un cran. On recharge la page, et le compteur tient, le cœur reste plein. Rien de spectaculaire.

Il y a quelques années, j'aurais sorti jQuery sans réfléchir. C'était le réflexe sous WordPress, et ça marche. Mais depuis WordPress 6.5, il existe une façon native de faire ça, l'Interactivity API, et pour ce genre d'interaction elle est franchement plus propre. Je reprends ce bouton de bout en bout : le clic, l'affichage, et la persistance côté serveur.

C'est volontairement un cas simple, pour poser les bases. Un seul bouton, un seul état. Pas de quoi sortir l'artillerie.


Le réflexe jQuery

Voilà à quoi ressemblait ma version d'avant. On accroche un gestionnaire de clic sur le bouton, on envoie la requête au serveur, et dans la réponse on remet l'affichage à jour.

jQuery( '.like-btn' ).on( 'click', function () {
    var $btn = jQuery( this );

    jQuery.post( ajaxurl, {
        action: 'like_toggle',
        post_id: $btn.data( 'id' ),
        nonce: $btn.data( 'nonce' )
    }, function ( res ) {
        $btn.toggleClass( 'is-liked', res.liked );
        $btn.find( '.like-count' ).text( res.count );
        $btn.attr( 'aria-pressed', res.liked );
    } );
} );

Ça fonctionne. Mais regardez le callback : après la réponse du serveur, je remets à jour la classe, puis le texte du compteur, puis l'attribut aria-pressed, un par un, à la main. Le jour où j'ajoute un détail à l'écran qui dépend de l'état du bouton, c'est une ligne de plus à ne jamais oublier dans ce callback. La vérité de l'affichage est éparpillée dans une suite d'instructions.


L'idée : décrire le comportement dans le HTML

L'Interactivity API renverse la logique. Au lieu d'aller chercher des éléments pour les modifier, on déclare dans le balisage à quoi chaque élément est lié. Le clic appelle une action, le texte suit une valeur, une classe dépend d'un état. On écrit le « quoi », l'API s'occupe du « comment ».

En interne, l'API s'appuie sur Preact et un système de signaux, mais on n'écrit pas de composants, pas de JSX. Pas besoin de connaître React. Le balisage reste du HTML, et la logique tient dans un petit objet JavaScript : le store.


Le bouton, en HTML déclaratif

Voici le bouton tel que le serveur le rend. Tout son comportement est posé en attributs.

<div
    data-wp-interactive="post-like"
    data-wp-context='{ "postId": 42, "liked": false, "count": 12, "nonce": "a1b2c3", "ajaxUrl": "/wp-admin/admin-ajax.php" }'
    data-wp-class--is-liked="context.liked"
>
    <button
        data-wp-on--click="actions.toggle"
        data-wp-bind--aria-pressed="context.liked"
        data-wp-bind--aria-label="state.ariaLabel"
    >
        <span class="like-icon" aria-hidden="true"></span>
        <span class="like-count" data-wp-text="context.count"></span>
    </button>
</div>

Chaque attribut a un rôle précis :

  • data-wp-interactive active la zone et lui donne son nom, ici post-like. C'est ce qui relie le balisage au bon store.
  • data-wp-context porte les données propres à ce bouton : l'identifiant du post, l'état liké ou non, le compteur, et de quoi parler au serveur.
  • data-wp-on--click branche le clic sur l'action toggle. Aucun addEventListener à écrire.
  • data-wp-text fait suivre au compteur la valeur context.count. Elle change, le texte change.
  • data-wp-bind--aria-pressed et data-wp-bind--aria-label tiennent l'accessibilité à jour toute seule, ce qui était justement le genre de ligne qu'on oubliait en jQuery.

Le store, le cerveau du bouton

Le store regroupe l'état et les actions. Ici il tient en quelques lignes.

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

const { state } = store( 'post-like', {
    state: {
        get ariaLabel() {
            const context = getContext();
            return context.liked
                ? `Retirer le j'aime (${ context.count })`
                : `Ajouter un j'aime (${ context.count })`;
        },
    },
    actions: {
        *toggle() {
            const context = getContext();

            // mise à jour optimiste : on change l'affichage tout de suite
            context.liked = ! context.liked;
            context.count += context.liked ? 1 : -1;

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

            // on réaligne sur la réponse du serveur
            context.liked = data.liked;
            context.count = data.count;
        },
    },
} );

Deux choses méritent qu'on s'arrête. D'abord ariaLabel est un getter : dès que context.count ou context.liked change, le label se recalcule sans qu'on ait à le demander. Ensuite l'action est une fonction génératrice, repérable au * et aux yield. C'est la façon dont l'API gère l'asynchrone : on écrit l'appel réseau comme s'il était synchrone, sans chaîne de promesses.

La mise à jour optimiste est un petit confort : on bascule l'affichage immédiatement, sans attendre le serveur, puis on réaligne sur sa réponse. Si le réseau traîne, le visiteur a quand même un retour instantané. Si le serveur renvoie autre chose, c'est lui qui a le dernier mot.


La persistance, côté serveur

Le compteur doit tenir entre deux visites. On le stocke dans une métadonnée du post, et l'action AJAX (un appel au serveur en arrière-plan, sans recharger la page) se charge de l'incrémenter.

add_action( 'wp_ajax_like_toggle', 'toggle_like' );
add_action( 'wp_ajax_nopriv_like_toggle', 'toggle_like' );

function toggle_like() {
    check_ajax_referer( 'like_nonce', 'nonce' );

    $post_id = absint( $_POST['post_id'] ?? 0 );
    $liked   = visitor_has_liked( $post_id ); // lu depuis un cookie
    $count   = (int) get_post_meta( $post_id, '_likes_count', true );

    $count = $liked ? max( 0, $count - 1 ) : $count + 1;
    update_post_meta( $post_id, '_likes_count', $count );
    remember_visitor_like( $post_id, ! $liked ); // écrit le cookie

    wp_send_json( [ 'liked' => ! $liked, 'count' => $count ] );
}

Le nonce vérifié en première ligne est un jeton anti-triche que WordPress génère et contrôle : il garantit que la requête vient bien de votre page. Le « déjà liké » d'un visiteur anonyme tient dans un simple cookie, ce qui évite d'imposer un compte.

Reste le plus important pour l'expérience : éviter que le bouton clignote au chargement. La réponse, c'est de remplir le contexte au moment du rendu, côté serveur.

$liked = visitor_has_liked( $post_id );
$count = (int) get_post_meta( $post_id, '_likes_count', true );

$context = wp_json_encode( [
    'postId'  => $post_id,
    'liked'   => $liked,
    'count'   => $count,
    'nonce'   => wp_create_nonce( 'like_nonce' ),
    'ajaxUrl' => admin_url( 'admin-ajax.php' ),
] );
// ce $context part dans l'attribut data-wp-context du bloc

Comme l'état part du serveur, le bouton s'affiche d'emblée dans le bon état : déjà liké si le cookie le dit, avec le bon compteur. Pas de cœur vide qui se remplit une demi-seconde plus tard. C'est ce que l'API appelle l'hydratation : la page arrive déjà juste, le JavaScript ne fait que reprendre la main.


Ce que jQuery demandait, ce que l'API fait à ma place

Reprenez le callback jQuery du début, celui qui remettait à jour la classe, le texte et l'attribut un par un. Il a disparu. Je n'écris plus comment mettre à jour l'écran, je déclare à quoi chaque élément est lié, et l'API s'occupe du reste.

Plus de sélecteur à écrire, plus de addEventListener, et surtout aucune fonction de redraw à rappeler à la main. Le compteur suit son contexte, la classe suit l'état, le label se recalcule tout seul. Pour un bouton, l'économie paraît modeste. Elle devient décisive dès que plusieurs éléments dépendent du même état, parce que c'est là que le code jQuery se met à diverger en silence.


Et après ? Quand un bouton ne suffit plus

Ce bouton ne parle qu'à lui-même : son état vit dans son propre contexte, il n'a rien à partager avec le reste de la page. C'est le cas le plus simple, et le bon point de départ.

Les choses deviennent plus intéressantes quand plusieurs zones éloignées de la page doivent rester d'accord en permanence : un bouton ici, un bandeau là, un tableau sur une autre page, tous synchronisés sur le même état. C'est le sujet d'un second article, où je décortique un comparateur de produits avec un état réellement partagé. Le modèle est le même, on monte juste d'un cran.

En attendant, si vous hésitez encore entre WordPress et un outil propriétaire pour votre site, je défends le choix du CMS dans cet article sur WordPress face aux SaaS. Et sur l'art de ne pas sur-outiller une interface, j'en parle dans mon article sur la sobriété numérique : l'Interactivity API en est un bon exemple, on obtient une vraie réactivité sans embarquer un framework entier sur le dos du visiteur.

Vous avez une interaction de ce genre à intégrer sur un site WordPress et vous vous demandez par où la prendre ? C'est le type d'arbitrage dont je discute volontiers : prenez quelques minutes en visio, on regarde votre cas ensemble.

Questions fréquentes

FAQ

L'Interactivity API, c'est quoi exactement ?

C'est l'interface native de WordPress, stable depuis la version 6.5 (2024), pour rendre un bloc interactif côté visiteur sans charger de framework JavaScript. On décrit le comportement directement dans le HTML, avec des attributs data-wp-*, reliés à un petit objet JavaScript appelé store. Pour un bouton, un filtre ou un compteur, ça suffit largement.

Est-ce que ça remplace vraiment jQuery ?

Pour la plupart des interactions front d'un site WordPress, oui. jQuery vous fait sélectionner des éléments puis redessiner le DOM à la main à chaque changement. L'Interactivity API maintient un état et met à jour l'affichage toute seule. jQuery reste présent dans beaucoup de thèmes anciens, mais pour du neuf, l'API native est un meilleur point de départ.

Faut-il créer un bloc Gutenberg pour s'en servir ?

C'est le terrain naturel : l'API est pensée pour les blocs. Le minimum, c'est WordPress 6.5 et un balisage auquel vous attachez les directives data-wp-*. Un bloc dynamique avec un fichier render.php est le cas le plus courant, et c'est celui de cet exemple.

Comment le compteur de j'aime survit-il au rechargement ?

Le nombre est stocké côté serveur, ici dans une métadonnée du post. Au rendu de la page, WordPress lit cette valeur et la place dans le contexte du bloc. Le bouton s'affiche donc déjà dans le bon état, sans appel réseau au chargement et sans clignotement.

L'Interactivity API est-elle adaptée à une grosse interface ?

Pour un site de contenu et des interactions ciblées, c'est l'outil juste. Pour une vraie application très dynamique, un tableau de bord avec des dizaines d'états liés, React garde du sens. L'idée n'est pas de tout remplacer, mais d'éviter d'embarquer un framework entier pour un bouton ou un filtre.

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.