Tu connais sûrement le tuto WordPress sur la création de pages d’options avec React et Gutenberg ? Bien que super utile, je te propose aujourd’hui une approche différente qui va te donner les pleins pouvoirs sur ton code.
Pourquoi cette approche ?
On va construire un truc plus costaud avec :
- React + Gutenberg pour une UI aux petits oignons 👌
- Notre propre API REST (bye bye les contraintes WordPress)
- Un système de save qui se fait tout seul
Le plan d’action
Plutôt que de suivre bêtement le chemin classique, on va :
- Créer une interface qui claque avec les composants Gutenberg
- Mettre en place notre endpoint d’API perso
- Construire un système évolutif qui nous facilitera la vie plus tard
Les points forts de cette méthode
En développant notre propre route API, on gagne en :
- Liberté totale sur la structure des données
- Contrôle complet des validations
- Flexibilité pour gérer les cas particuliers
- Maintenabilité à long terme
Ok, ça demande un peu plus de boulot au début. Mais crois-moi, quand ton projet va grandir, tu seras content d’avoir cette flexibilité !
Initialisation
On démarre comme le tuto WordPress, mais ensuite on va pimper tout ça !
add_action(
'admin_menu',
function () {
add_menu_page(
'React Test Plugin Settings',
'React Test Plugin 🐝',
'manage_options',
'react-test-plugin-auto',
function () {
echo '<div class="wrap"><div id="test-rest-root"></div></div>';
},
'dashicons-shield-alt'
);
}
);
add_action(
'admin_enqueue_scripts',
function () {
if ( ! isset( $_GET['page'] ) || $_GET['page'] !== 'react-test-plugin-auto' ) {
return;
}
$get_data_json = get_data_json_sk();
if ( empty( $get_data_json ) ) {
return;
}
$asset_file = plugin_dir_path( __FILE__ ) . 'js/index.asset.php';
if ( ! file_exists( $asset_file ) ) {
return;
}
// Enqueue Gutenberg block editor styles
wp_enqueue_style( 'wp-components' );
wp_enqueue_style( 'wp-editor' );
wp_enqueue_style( 'wp-edit-post' );
wp_enqueue_style( 'wp-block-editor' );
wp_enqueue_style( 'wp-format-library' );
wp_enqueue_style( 'wp-nux' );
$asset = include $asset_file;
wp_enqueue_script(
'test_root_admin_script_auto',
plugins_url( 'js/index.js', __FILE__ ),
$asset['dependencies'],
$asset['version'],
array(
'in_footer' => true,
)
);
wp_localize_script(
'test_root_admin_script_auto',
'ROOT_ADMIN',
array(
'nonce' => wp_create_nonce( 'wp_rest' ),
'dataJson' => json_encode( $get_data_json ),
)
);
wp_enqueue_style(
'test_root_admin_style_auto',
plugins_url( 'js/index.css', __FILE__ ),
array(),
$asset['version']
);
}
);
Les vérifications de base
On met en place nos gardes-fous :
- Check si on est sur la bonne page avec
$_GET['page'] - Vérifie que nos données JSON ne sont pas vides
- S’assure que notre fichier d’assets existe
La partie stylée 🎨
Petite astuce de pro :
- On récupère directement les styles Gutenberg (pourquoi réinventer la roue ?)
- Résultat : une interface pro sans se prendre la tête en CSS
Le setup des scripts
On balance notre artillerie :
- Injection de notre script principal
wp_localize_scriptpour notre route API custom- Passage de nos champs à créer côté JS
La touche finale
Un petit fichier CSS perso, parce que :
- Y’aura toujours un truc à ajuster
- Faut bien mettre sa patte quelque part !
Pro tip : Cette structure te permet d’avoir une base solide pour la suite. On fait les choses proprement dès le début ! 😉
La function data_json : Le coeur de notre système
Le Concept 🎯
On crée une fonction qui va gérer la configuration de nos champs. Simple mais efficace !
Les types de champs supportés
Le système gère automatiquement :
- Input text (le classique)
- Checkbox (pour les choix binaires)
- Toggle (plus sexy que la checkbox)
- Select (pour les listes déroulantes)
- Textarea (pour les textes longs)
- Radio (quand il faut choisir UNE option)
Comment ça marche ?
- On définit une config de base
- On permet l’override via un hook
- On nettoie automatiquement les types non supportés (pour le moment c’est 💩 mais implémente le)
Attention ⚠️
Pour l’instant, pas de système de protection ! Si tu joues au petit malin avec le hook et que tu envoies n’importe quoi… ça va partir en cacahuète !
Mais t’inquiète, la structure est là pour ajouter les validations plus tard. On pose les bases, on sécurisera après ! (Enfin tu sécuriseras 😜)
function get_data_json_sk() {
$default = array(
'options' => array(
'suffix' => 'default',
),
'fields' => array(
'who_default_field_text' => array(
'label' => 'Je suis un input text de test',
'value' => '',
'type' => 'text',
'placeholder' => 'Je suis un placeholder',
),
'who_default_field_toogle' => array(
'label' => 'Je suis un toogle',
'value' => '',
'type' => 'toogle',
),
'who_default_field_toogle2' => array(
'label' => 'Je suis un toogle activé par défaut',
'value' => 1,
'type' => 'toogle',
),
'who_default_field_textarea' => array(
'label' => 'Je suis un textarea',
'value' => '',
'type' => 'textarea',
'placeholder' => 'Je suis une placeholder',
'rows' => 5,
),
'who_default_field_checkbox' => array(
'label' => 'Je suis une checkbox',
'value' => 0,
'type' => 'checkbox',
),
'who_default_field_checkbox2' => array(
'label' => 'Je suis une checkbox activé par défaut',
'value' => 1,
'type' => 'checkbox',
),
'who_default_field_select' => array(
'label' => 'Je suis un select',
'value' => 0,
'type' => 'select',
'options' => array(
array(
'value' => 0,
'label' => 'Select option',
'disabled' => true,
),
array(
'value' => 'option1',
'label' => 'Option 1',
),
array(
'value' => 'option2',
'label' => 'Option 2',
),
),
),
),
);
$data = (array) apply_filters( 'test_root_admin_save_settings_auto', $default );
$data = wp_parse_args( $data, $default );
if ( ! is_array( $data ) ) {
$data = array();
}
return $data;
}
On couvre déjà les champs les plus courants, ce qui est plutôt pas mal !
Si tu veux des types de champs plus exotiques, va falloir mettre les mains dans le cambouis. Mais la base est là, à toi de jouer !
On passe aux routes REST ? 😎
add_action( 'rest_api_init',
function () {
register_rest_route( 'sk-audo-admin/v1', '/save-settings', array(
'methods' => 'POST',
'callback' => function ( \WP_REST_Request $request ) {
$data = get_data_json();
$name_options = 'who_settings_' . $data['options']['suffix'];
$saveOptions = get_option( $name_options, array() );
$params = $request->get_params();
foreach ( $params as $key => $param ) {
if ( ! isset( $data['fields'][ $key ] ) ) {
continue;
}
$saveOptions[ $key ] = $param;
}
// save options
update_option( $name_options, $saveOptions );
return new \WP_REST_Response( array( 'success' => 1 ), 200 );
},
'permission_callback' => function ( \WP_REST_Request $request ) {
$nonce = $request->get_header( 'X-WP-Nonce' );
$verify = 1 === wp_verify_nonce( $nonce, 'wp_rest' );
return apply_filters( 'test_root_admin_permission_callback_auto', $verify, $request );
},
) );
register_rest_route( 'sk-audo-admin/v1', '/settings', array(
'methods' => 'GET',
'callback' => function ( \WP_REST_Request $request ) {
$data = get_data_json();
$name_options = 'who_settings_' . $data['options']['suffix'];
$saveOptions = get_option( $name_options, array() );
return new \WP_REST_Response( $saveOptions, 200 );
},
'permission_callback' => function ( \WP_REST_Request $request ) {
$nonce = $request->get_header( 'X-WP-Nonce' );
$verify = 1 === wp_verify_nonce( $nonce, 'wp_rest' );
return apply_filters( 'test_root_admin_permission_callback_auto', $verify, $request );
},
) );
}
);
La route « save-settings » 💾
Super simple dans son approche :
- On prend la liste de nos champs autorisés
- On compare avec ce qui arrive en POST
- Si ça correspond, on garde et on sauvegarde
- Sinon, bye bye !
Pas de chichi pour l’instant : pas de validation de champs requis ou de règles complexes. C’est basique, mais ça pose les bases pour évoluer tranquillou !
La route « settings » 📖
Encore plus basique que la première :
- Elle récupère juste nos options
- Les envoie au back-office
- Point barre !
Et c’est tout ! Parfois, le plus simple est le plus efficace 😎
REACT time ! 🎨
Comment qu’on fait ?
Les composants Gut qu’on utilise
- Input text (classique)
- Checkbox (on/off)
- Toggle (le switch stylé)
- Select (la liste déroulante)
- Textarea (pour les romans)
- Radio (choix unique)
La config de nos champs
On fait matcher nos besoins avec les composants :
- Mapping entre notre config PHP
- Et les composants React qu’on a choisis
Construction de la page
C’est là que tout s’assemble :
- Structure de base
- Placement des composants
- Gestion des interactions
import {createRoot, useEffect, Fragment, useState} from "@wordpress/element";
import {__} from '@wordpress/i18n';
import apiFetch from '@wordpress/api-fetch';
import './css/index.scss';
const ROOT_API_PATH = 'sk-audo-admin/v1';
let dataJson = JSON.parse(ROOT_ADMIN.dataJson);
let components = {
'text': 'TextControl',
'checkbox': 'CheckboxControl',
'toogle': 'ToggleControl',
'select': 'SelectControl',
'textarea': 'TextareaControl',
'radio': 'RadioControl',
}
// On vérifie que chaque champ configuré correspond à nos types supportés
// Si ça match pas, on le vire
// Pas de compromis : soit c'est dans la liste des possibles, soit ça dégage !
Object.entries(dataJson.fields).map(([key, field]) => {
if (!components[field.type]) {
delete dataJson.fields[key];
}
});
// Pas de champs ? Pas de page ! Pas de page ? Pas de code ! Pas de code... Bah pas de code, hein ! 🤷
if (Object.keys(dataJson.fields).length === 0) {
throw new Error('dataJson.fields is empty');
}
import {
ToggleControl,
Icon,
Button,
Spinner,
CheckboxControl,
TextControl,
SelectControl,
TextareaControl,
RadioControl
} from '@wordpress/components';
const App = () => { } // La logique : c'est ici que ça devient intéressant ! 💪
// Ok, tout est prêt ? Tout est validé ? Alors BOOM : on lance l'app ! 🚀
document.addEventListener('DOMContentLoaded', () => {
const htmlOutput = document.getElementById('test-rest-root');
if (htmlOutput) {
const root = createRoot(htmlOutput);
root.render(
<App/>
);
}
});
- On commence par les classiques : « Allez, ramène tous les imports qu’on va avoir besoin ! »
- Petite config rapide : « 2-3 réglages vite fait, bien fait »
- Les composants, c’est pas open bar : « On définit la liste des composants autorisés, et on déconne pas avec ça ! »
- Phase de contrôle : « On vérifie que tout est nickel chrome »
- Le moment de vérité : « Tout est ok ? On balance la page ! »
L’App 🎯
const App = () => {
const [isDisabledButton, setIsDisabledButton] = useState(false);
const [isNotice, setNotice] = useState(false);
const [isLoading, setIsLoading] = useState(true);
// 1. On démarre à zéro : init du state !
const [state, setState] = useState(
Object.fromEntries(
Object.entries(dataJson.fields).map(([key, field]) => [key, field.value])
)
);
const headers = new Headers({
'Content-Type': 'application/json',
'X-WP-Nonce': ROOT_ADMIN.nonce,
});
// 2. On check si y'a déjà des trucs en stock
const setData = () => {
setIsLoading(true);
apiFetch({
path: ROOT_API_PATH + '/settings',
method: 'GET',
}).then((res) => {
setState(prevState => ({...prevState, ...res}));
}).finally(() => {
setIsLoading(false);
});
}
// 3. L'app se lance, on récup tout ce qu'on a besoin
useEffect(() => {
setData();
}, []);
// 4. Un truc bouge ? On réagit !
const handleChange = (key) => (value) => {
setState(prevState => ({...prevState, [key]: value}));
};
// 5. Click de save ? On balance tout en base !
const saveSettings = () => {
setIsDisabledButton(true);
apiFetch({
path: ROOT_API_PATH + '/save-settings',
method: 'POST',
headers: headers,
data: state,
}).then((res) => {
setNotice(true);
// après 1 seconde, pouff plus de notice
setTimeout(() => {
setNotice(false);
setIsDisabledButton(false);
}, 1000);
});
}
// 6. La partie fun : on balance nos champs !
let dataJsonArray = Object.entries(dataJson.fields).map(([key, field]) => {
if (field.type === 'text') {
return (
<TextControl
key={key}
placeholder={field.placeholder ? field.placeholder : ''}
label={field.label}
value={state[key]}
onChange={handleChange(key)}
/>
);
}
if (field.type === 'checkbox') {
return (
<CheckboxControl
key={key}
label={field.label}
checked={state[key]}
onChange={handleChange(key)}
/>
);
}
if (field.type === 'toogle') {
return (
<ToggleControl
key={key}
label={field.label}
checked={state[key]}
onChange={handleChange(key)}
/>
);
}
if (field.type === 'select') {
return (
<SelectControl
key={key}
label={field.label}
value={state[key]}
options={field.options}
onChange={handleChange(key)}
/>
);
}
if (field.type === 'textarea') {
return (
<TextareaControl
key={key}
placeholder={field.placeholder ? field.placeholder : ''}
rows={field.rows ? field.rows : 3}
label={field.label}
value={state[key]}
onChange={handleChange(key)}
/>
);
}
if (field.type === 'radio') {
return (
<RadioControl
key={key}
label={field.label}
selected={state[key]}
options={field.options}
onChange={handleChange(key)}
/>
);
}
});
return (
<Fragment>
<h1>{__('Settings', 'test-rest-root')}</h1>
{isLoading ? (
<div className="loader">
<Spinner/>
</div>
) : (
<Fragment>
{dataJsonArray}
<Button
onClick={() => {
saveSettings();
}}
className={isNotice ? 'is-success-whoSettings' : ''}
disabled={isDisabledButton}
isPrimary
>{isNotice ?
<Fragment>
{__('Saved', 'who')} <Icon icon="yes" size={30}/>
</Fragment>
:
__('Save', 'who')}
{isDisabledButton && !isNotice && (
<Spinner/>
)}
</Button>
</Fragment>
)}
</Fragment>
);
};
La base de notre app
- On part sur du useState comme des pros :
- D’abord les flags true/false
- Et on boucle sur tous les champs comme des malades ! 🔄
- Direction la BDD, on ramène le butin !
- useEffect aux commandes : dès que l’app démarre, on charge tout
- La fonction magique d’update : elle gère l’avant/après comme un chef
- Le bouton save qui fait vroum : un click et c’est parti !
- Création de tous nos champs, un par un
La Cerise sur le gâteau 🍒
Le return :
- App pas prête ? → Petit loader qui tourne
- Tout est ok ? → BAM ! Les champs + le bouton save
Et voilà le travail ! Plutôt clean non ? 😎

Disclaimer de Fin 🎯
« Ça marche chez moi » – la phrase la plus dangereuse du dev ! 😅, mais chez toi …. qui sait ! 🤷♂️
Toi-même tu sais ! 😉

