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-interactiveactive la zone et lui donne son nom, icipost-like. C'est ce qui relie le balisage au bon store.data-wp-contextporte 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--clickbranche le clic sur l'actiontoggle. AucunaddEventListenerà écrire.data-wp-textfait suivre au compteur la valeurcontext.count. Elle change, le texte change.data-wp-bind--aria-pressedetdata-wp-bind--aria-labeltiennent 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.