Le WordPress d'aujourd'hui, décodé pour les développeurs

E-commerce

useSelect et useDispatch pour afficher des recommandations produit dans l’éditeur de blocs

Un bloc d'upsell qui propose des produits complémentaires directement dans l'éditeur : voici comment lire et modifier son état de réglages avec les hooks React de WordPress.

Par WordPress Développement • 30 septembre 2026 • 7 min de lecture • Aucun commentaire
useSelect et useDispatch pour afficher des recommandations produit dans l'éditeur de blocs

« Quels produits complémentaires afficher dans ce bloc d’upsell ? » Cette question, un développeur front doit pouvoir y répondre directement dans l’éditeur, sans recharger l’aperçu du site à chaque changement de réglage. C’est exactement le problème posé par un bloc d’upsell personnalisé, construit pour proposer jusqu’à trois produits complémentaires configurés à la main par l’éditeur de contenu.

La difficulté ne se situe pas dans l’affichage front du bloc, plutôt classique, mais dans la gestion de son état pendant l’édition : chargement de la liste des produits, sélection multiple, prévisualisation en direct dans le panneau de réglages latéral.

Le rôle de useSelect dans ce bloc

Le hook useSelect, fourni par @wordpress/data, permet de lire un état issu d’un store, tout en re-rendant le composant uniquement quand la donnée observée change réellement. Dans ce bloc d’upsell, il sert à interroger le store core pour récupérer la liste des produits disponibles au fur et à mesure que l’éditeur tape une recherche.

import { useSelect } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';

function useProduitsDisponibles( recherche ) {
    return useSelect( ( select ) => {
        if ( ! recherche ) {
            return [];
        }
        return select( coreStore ).getEntityRecords( 'postType', 'product', {
            search: recherche,
            per_page: 10,
        } );
    }, [ recherche ] );
}

Le tableau de dépendances passé en second argument, ici [ recherche ], joue un rôle équivalent à celui d’un useEffect classique : sans lui, useSelect continuerait à s’exécuter à chaque rendu, sans profiter de la mémorisation qu’offre normalement ce hook.

Le panneau de réglages avec InspectorControls

L'essentiel à retenir : useSelect() lit un état réactif sans provoquer de rendu superflu ; useDispatch() isole les actions d'un composant de son état ; Le panneau de réglages InspectorControls reste le meilleur endroit pour ce type de configuration

La sélection des produits complémentaires vit dans un InspectorControls, le panneau latéral standard de l’éditeur de blocs. C’est l’emplacement le plus cohérent pour ce type de configuration, parce qu’il ne perturbe jamais la mise en page du contenu principal pendant que l’éditeur ajuste ses choix.

Le bloc stocke ses choix dans un attribut de type tableau d’entiers, déclaré dans block.json :

{
    "apiVersion": 3,
    "name": "boutique/upsell",
    "title": "Produits complémentaires",
    "category": "widgets",
    "attributes": {
        "produitIds": {
            "type": "array",
            "items": { "type": "integer" },
            "default": []
        }
    },
    "editorScript": "file:./index.js"
}

Le composant d’édition s’appuie sur le crochet useProduitsDisponibles() défini plus haut, sur useDispatch() pour prévenir l’éditeur, et sur setAttributes() pour enregistrer la sélection. Le store de notifications de WordPress, @wordpress/notices, affiche un message bref en bas de l’écran quand l’éditeur tente de dépasser trois produits :

import { __ } from '@wordpress/i18n';
import { useState } from '@wordpress/element';
import { useSelect, useDispatch } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';
import { store as noticesStore } from '@wordpress/notices';
import { InspectorControls, useBlockProps } from '@wordpress/block-editor';
import { PanelBody, SearchControl, CheckboxControl, Spinner } from '@wordpress/components';

const MAX_PRODUITS = 3;

export default function Edit( { attributes, setAttributes } ) {
    const { produitIds } = attributes;
    const [ recherche, setRecherche ] = useState( '' );

    const resultats = useProduitsDisponibles( recherche );
    const enChargement = recherche !== '' && resultats === null;

    const selection = useSelect(
        ( select ) =>
            produitIds.length
                ? select( coreStore ).getEntityRecords( 'postType', 'product', {
                      include: produitIds,
                      per_page: MAX_PRODUITS,
                  } )
                : [],
        [ produitIds ]
    );

    const { createNotice } = useDispatch( noticesStore );

    const basculer = ( id, coche ) => {
        if ( coche && produitIds.length >= MAX_PRODUITS ) {
            createNotice(
                'warning',
                __( 'Trois produits au maximum.', 'boutique-upsell' ),
                { type: 'snackbar', isDismissible: true }
            );
            return;
        }
        setAttributes( {
            produitIds: coche
                ? [ ...produitIds, id ]
                : produitIds.filter( ( autre ) => autre !== id ),
        } );
    };

    return (
        <div { ...useBlockProps() }>
            <InspectorControls>
                <PanelBody title={ __( 'Produits proposés', 'boutique-upsell' ) }>
                    <SearchControl
                        label={ __( 'Rechercher un produit', 'boutique-upsell' ) }
                        value={ recherche }
                        onChange={ setRecherche }
                    />
                    { enChargement && <Spinner /> }
                    { ( resultats || [] ).map( ( produit ) => (
                        <CheckboxControl
                            key={ produit.id }
                            label={ produit.title.rendered }
                            checked={ produitIds.includes( produit.id ) }
                            onChange={ ( coche ) => basculer( produit.id, coche ) }
                        />
                    ) ) }
                </PanelBody>
            </InspectorControls>
            <ul>
                { ( selection || [] ).map( ( produit ) => (
                    <li key={ produit.id }>{ produit.title.rendered }</li>
                ) ) }
            </ul>
        </div>
    );
}

Chaque crochet joue son rôle. useSelect() lit, deux fois : les résultats de la recherche (dans le crochet réutilisable) et la sélection courante, récupérée par leurs identifiants grâce au paramètre include. useDispatch() ne lit rien : il renvoie des fonctions d’action, ici createNotice, que le composant déclenche sans connaître l’implémentation du store. Quant à setAttributes(), il n’est pas une action de store : c’est la fonction fournie au composant pour modifier les attributs du bloc.

Ce que useSelect fait vraiment, et ce qu’il ne fait pas

  • Il relance la fonction passée en argument quand le store change, puis ne redessine le composant que si le résultat retourné est différent (comparaison superficielle).
  • Il ne doit pas déclencher d’effet de bord. Appeler dispatch() à l’intérieur de la fonction de sélection est une erreur : la lecture peut s’exécuter très souvent.
  • Il ne remplace pas l’état local. Le texte de la recherche est propre à ce composant : useState() convient, inutile de le placer dans un store.
  • Il ne met pas en cache à votre place la réponse réseau. C’est le résolveur de getEntityRecords qui émet la requête une seule fois par combinaison de paramètres ; le crochet se contente de lire l’état du store.

Un cas concret : limiter les requêtes pendant la frappe

Tel quel, chaque lettre tapée dans le champ de recherche change le paramètre search et déclenche une nouvelle requête REST. Le paquet @wordpress/compose fournit useDebounce(), qui retarde l’appel jusqu’à ce que la frappe s’interrompe :

import { useDebounce } from '@wordpress/compose';

// Dans le composant :
const [ recherche, setRecherche ] = useState( '' );
const [ rechercheRetardee, setRechercheRetardee ] = useState( '' );
const appliquer = useDebounce( setRechercheRetardee, 400 );

const surSaisie = ( valeur ) => {
    setRecherche( valeur );
    appliquer( valeur );
};

// useProduitsDisponibles( rechercheRetardee ) remplace l'appel précédent ;
// le champ reste réactif, la requête part 400 ms après la dernière touche.

Le champ affiche immédiatement ce que l’éditeur tape, tandis que la requête ne part qu’avec la valeur retardée. C’est une amélioration de confort qui évite aussi d’encombrer le serveur de requêtes inutiles.

Un bon réglage d’éditeur se remarque à ce qu’il ne fait pas : pas de rechargement, pas d’attente, pas de surprise.

Les pièges fréquents

  • Oublier le tableau de dépendances quand la fonction de sélection utilise une variable externe : la valeur lue reste celle du premier rendu.
  • Confondre null et tableau vide. getEntityRecords() renvoie null pendant le chargement et un tableau vide quand il n’y a aucun résultat ; afficher « aucun produit » pendant le chargement trompe l’éditeur.
  • Supposer que le type d’entrée product est exposé. Il l’est avec WooCommerce, mais un type de contenu personnalisé doit déclarer show_in_rest pour être lisible depuis l’éditeur.
  • Stocker l’objet produit entier dans les attributs. Conservez seulement l’identifiant ; le titre, le prix ou l’image doivent rester lisibles à jour depuis le store et le rendu côté serveur.

Conclusion

Dans un bloc d’upsell, la séparation est simple : useSelect() lit, useDispatch() agit, setAttributes() mémorise, et le panneau InspectorControls héberge la configuration sans perturber la mise en page du contenu. Avec un attribut ne contenant que des identifiants, un crochet de lecture réutilisable et une saisie retardée, le bloc reste réactif et léger, et l’éditeur de contenu voit ses choix sans jamais recharger l’aperçu.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Partager :

À propos de l'auteur

WordPress Développement

Développeur WordPress, passionné par Elementor, le FSE et l’automatisation par IA.

Voir tous ses articles

Dans la même veine

À lire aussi