Contents
Introducción
Este artículo explica, paso a paso y con todo lujo de detalles, cómo crear una pantalla de opciones en el panel de administración de WordPress usando React y el paquete @wordpress/data. Veremos desde la estructura mínima del plugin, cómo exponer una API REST segura para leer/escribir las opciones, cómo registrar y usar un store de datos con @wordpress/data, y cómo construir un componente React que consuma ese store usando useSelect y useDispatch. También cubriremos aspectos de seguridad, internacionalización y buenas prácticas de rendimiento.
Requisitos previos
- WordPress 5.5 (o superior) con soporte para los paquetes oficiales de Gutenberg.
- Node.js y npm/yarn para compilar el JavaScript con @wordpress/scripts o su propia configuración de bundler.
- Conocimientos básicos de React, REST API y WordPress (hooks, opciones, roles).
Estructura mínima del plugin
Se recomienda esta organización de archivos:
- my-plugin/
- my-plugin.php (archivo principal del plugin)
- build/
- index.js (bundle compilado por webpack/@wordpress/scripts)
- src/
- index.js (código fuente React store)
- store.js (implementación del store con @wordpress/data)
- options-screen.js (componente React)
Paso 1 — PHP: Registrar la página de administración, encolar scripts y exponer REST API
Vamos a crear un endpoint REST para leer y actualizar la opción, y luego encolaremos el bundle JS en la pantalla de opciones.
Archivo principal del plugin (my-plugin.php):
lt?php / Plugin Name: My Options via React @wordpress/data Version: 1.0 / defined( ABSPATH ) exit add_action( admin_menu, my_plugin_add_menu ) function my_plugin_add_menu() { add_menu_page( My Plugin Options, My Plugin, manage_options, my-plugin-options, my_plugin_options_page_markup ) } add_action( admin_enqueue_scripts, my_plugin_enqueue_admin_assets ) function my_plugin_enqueue_admin_assets( hook ) { // Asegúrate de que sólo se cargue en nuestra página if ( hook !== toplevel_page_my-plugin-options ) { return } // Ruta al bundle compilado (build/index.js) script_handle = my-plugin-admin wp_register_script( script_handle, plugins_url( build/index.js, __FILE__ ), array( wp-element, wp-data, wp-api-fetch ), // dependencias de WP filemtime( plugin_dir_path( __FILE__ ) . build/index.js ), true ) // Datos para pasar al script: nonce y URL REST (si usas endpoint propio) rest_root = esc_url_raw( rest_url() ) nonce = wp_create_nonce( wp_rest ) wp_localize_script( script_handle, MyPluginSettings, array( root => rest_root, nonce => nonce, namespace => my-plugin/v1 ) ) wp_enqueue_script( script_handle ) } function my_plugin_options_page_markup() { // Contenedor donde React montará la app echo ltdiv id=my-plugin-options-rootgtlt/divgt } / REST API: registrar rutas para obtener/guardar opciones. / add_action( rest_api_init, my_plugin_register_rest_routes ) function my_plugin_register_rest_routes() { register_rest_route( my-plugin/v1, /options, array( array( methods => GET, callback => my_plugin_get_options, permission_callback => function() { return current_user_can( manage_options ) } ), array( methods => POST, callback => my_plugin_update_options, permission_callback => function() { return current_user_can( manage_options ) }, args => array( options => array( required => true, ), ), ), ) ) } function my_plugin_get_options( WP_REST_Request request ) { options = get_option( my_plugin_options, array( text_field => , enable_feature => false, ) ) return rest_ensure_response( options ) } function my_plugin_update_options( WP_REST_Request request ) { params = request->get_json_params() if ( ! is_array( params ) ! isset( params[options] ) ) { return new WP_Error( invalid_data, Missing options, array( status => 400 ) ) } options = params[options] // Sanitizar según tipos esperados sanitized = array() sanitized[text_field] = isset( options[text_field] ) ? sanitize_text_field( options[text_field] ) : sanitized[enable_feature] = ! empty( options[enable_feature] ) ? true : false update_option( my_plugin_options, sanitized ) return rest_ensure_response( sanitized ) }
Notas sobre el código PHP
- La ruta REST está bajo el namespace my-plugin/v1 y el endpoint /options.
- Se comprueba la capacidad con current_user_can(manage_options) para seguridad.
- Se usa wp_localize_script para dar a JS la URL raíz del REST y el nonce para autenticación con apiFetch.
Paso 2 — Preparar el entorno JS (npm, @wordpress/scripts)
Recomiendo usar @wordpress/scripts para simplificar la compilación. En package.json incluir un script build que genere build/index.js.
{ name: my-plugin, version: 1.0.0, scripts: { build: wp-scripts build, start: wp-scripts start }, devDependencies: { @wordpress/scripts: ^24.0.0 }, dependencies: { @wordpress/data: ^7.0.0, @wordpress/api-fetch: ^4.0.0, @wordpress/element: ^4.0.0 } }
Paso 3 — Implementar un store con @wordpress/data
La idea es crear un store registrado en la librería de datos de WordPress que exponga acciones y selectores para obtener y actualizar las opciones. Usaremos controles para integrar llamadas asíncronas con apiFetch.
Ejemplo de store (src/store.js):
import { registerStore } from @wordpress/data import apiFetch from @wordpress/api-fetch / Types / const STORE_NAME = my-plugin/options / Actions / const actions = { fetchOptions() { return { type: FETCH_OPTIONS, } }, receiveOptions( options ) { return { type: RECEIVE_OPTIONS, options, } }, updateOptions( options ) { return { type: UPDATE_OPTIONS, options, } }, receiveUpdate( options ) { return { type: RECEIVE_UPDATE, options, } }, setError( error ) { return { type: SET_ERROR, error, } } } / Controls: para llamadas asíncronas / const controls = { FETCH_OPTIONS: () => { return apiFetch( { path: /my-plugin/v1/options } ) }, UPDATE_OPTIONS: ( { options } ) => { return apiFetch( { path: /my-plugin/v1/options, method: POST, data: { options } } ) } } / Reducer / const DEFAULT_STATE = { options: null, isLoading: false, isSaving: false, error: null, } function reducer( state = DEFAULT_STATE, action ) { switch ( action.type ) { case FETCH_OPTIONS: return { ...state, isLoading: true, error: null } case RECEIVE_OPTIONS: return { ...state, options: action.options, isLoading: false } case UPDATE_OPTIONS: return { ...state, isSaving: true, error: null } case RECEIVE_UPDATE: return { ...state, options: action.options, isSaving: false } case SET_ERROR: return { ...state, error: action.error, isLoading: false, isSaving: false } default: return state } } / Selectors / const selectors = { getOptions( state ) { return state.options }, isLoading( state ) { return state.isLoading }, isSaving( state ) { return state.isSaving }, getError( state ) { return state.error } } / Resolvers: disparan acciones asíncronas si es necesario / const resolvers = { getOptions() { // Si ya están cargadas, no volver a cargarlas const store = yield { type: GET_STATE } // para ejemplo conceptual } } / Registramos el store / registerStore( STORE_NAME, { reducer, actions, selectors, controls, } ) export default STORE_NAME
Explicación
- Usamos registerStore para exponer el store con nombre my-plugin/options.
- Las acciones FETCH_OPTIONS y UPDATE_OPTIONS están asociadas a controles que usan apiFetch para interactuar con la REST API.
- El reducer mantiene el estado: opciones, isLoading/isSaving y errores.
- En un proyecto real conviene añadir resolvers para cacheo y lógica más declarativa (aquí dejamos un enfoque simple y directo).
Paso 4 — Componente React que usa useSelect y useDispatch
Crearemos un componente que lea el estado del store con useSelect y dispare acciones con useDispatch. Mostraremos estados de carga, errores y formulario para editar las opciones.
Ejemplo (src/options-screen.js):
import { useEffect, useState } from @wordpress/element import { useSelect, useDispatch } from @wordpress/data import STORE_NAME from ./store export default function OptionsScreen() { const { options, isLoading, isSaving, error } = useSelect( ( select ) => { const store = select( STORE_NAME ) return { options: store.getOptions(), isLoading: store.isLoading(), isSaving: store.isSaving(), error: store.getError(), } }, [] ) const { fetchOptions, updateOptions } = useDispatch( STORE_NAME ) const [ localOptions, setLocalOptions ] = useState( { text_field: , enable_feature: false, } ) useEffect( () => { // Si aún no tenemos opciones, las pedimos if ( ! options ! isLoading ) { fetchOptions() } }, [ options, isLoading, fetchOptions ] ) useEffect( () => { if ( options ) { setLocalOptions( options ) } }, [ options ] ) function onChangeField( e ) { const { name, type, value, checked } = e.target setLocalOptions( ( prev ) => ( { ...prev, [ name ]: type === checkbox ? checked : value, } ) ) } function onSave( e ) { e.preventDefault() // Accion que dispara el control UPDATE_OPTIONS updateOptions( localOptions ) } if ( isLoading ! options ) { return ltdivgtCargando opciones...lt/divgt } return ( ltdiv className=my-plugin-optionsgt ltform onSubmit={ onSave }gt ltdivgt ltlabelgtTexto:lt/labelgt ltinput type=text name=text_field value={ localOptions.text_field } onChange={ onChangeField } /gt lt/divgt ltdivgt ltlabelgt ltinput type=checkbox name=enable_feature checked={ !! localOptions.enable_feature } onChange={ onChangeField } /gt Habilitar característica lt/labelgt lt/divgt ltdivgt ltbutton type=submit disabled={ isSaving }gt { isSaving ? Guardando... : Guardar cambios } lt/buttongt lt/divgt { error ltdiv className=errorgtError: { error.message Error desconocido }lt/divgt } lt/formgt lt/divgt ) }
Paso 5 — Punto de entrada: montar React y asegurarse que apiFetch use nonce
En index.js importamos el store y el componente, configuramos apiFetch para enviar el nonce (obtenido via wp_localize_script) y montamos la app en el div creado por PHP.
import { render } from @wordpress/element import apiFetch from @wordpress/api-fetch import STORE_NAME from ./store import OptionsScreen from ./options-screen import { registerStore } from @wordpress/data // ya usado en store.js // Configurar apiFetch para usar el nonce pasado desde PHP if ( typeof MyPluginSettings !== undefined ) { apiFetch.use( ( options, next ) => { options.headers = { ...options.headers, X-WP-Nonce: MyPluginSettings.nonce, } return next( options ) } ) } const root = document.getElementById( my-plugin-options-root ) if ( root ) { render( ltOptionsScreen /gt, root ) }
Paso 6 — Bundling y dependencias
Al compilar con @wordpress/scripts, asegúrate de marcar wp-data, wp-element y wp-api-fetch como external en tu configuración para que el bundle no incluya duplicados de las bibliotecas de WordPress. Con la configuración por defecto de @wordpress/scripts esto se maneja si declaras las dependencias correctas al registrar el script (como hicimos en PHP).
Paso 7 — Seguridad, sanitización y capacidades
- En el endpoint REST valida capacidades con permission_callback.
- Sanitiza todos los campos antes de guardar usando las funciones de WordPress (sanitize_text_field, intval, wp_kses_post si aceptas HTML, etc.).
- Protege el acceso a la pantalla con current_user_can al registrar el menú y en el callback del REST.
- Para acciones sensibles, considera usar nonces adicionales o la cabecera de nonce de WP REST (X-WP-Nonce), que apiFetch ya soporta si la configuras.
Paso 8 — Buenas prácticas y mejoras avanzadas
- Caché y selectores derivados: implementar resolvers y obtener datos sólo cuando es necesario.
- Optimistic updates: actualizar el estado localmente antes de la respuesta y revertir si falla.
- Validación en cliente y servidor: validar antes de enviar y también en el servidor por seguridad.
- Internacionalización: pasar strings traducibles desde PHP usando wp_set_script_translations o convertir strings en el bundle con @wordpress/i18n.
- Tests: crear pruebas unitarias para el reducer y las acciones.
Resumen completo del flujo
El flujo general es:
- PHP registra la página admin, encola el JS y expone rutas REST seguras.
- JS configura apiFetch con el nonce para autenticar llamadas REST.
- Se registra un store con @wordpress/data que define acciones, reducers y controls para llamadas asíncronas.
- El componente React usa useSelect para leer el estado y useDispatch para disparar acciones (fetch/update).
- La UI muestra estados (cargando, guardando, errores) y permite editar y guardar opciones.
Ejemplo mínimo de flujo de llamadas
1) Usuario abre la página → React monta → useEffect dispara fetchOptions → control FETCH_OPTIONS usa apiFetch GET /my-plugin/v1/options → reducer guarda opciones.
2) Usuario modifica formulario y pulsa Guardar → dispatch( updateOptions ) → control UPDATE_OPTIONS hace POST /my-plugin/v1/options con datos → servidor valida y guarda → respuesta con opciones actualizadas → reducer actualiza estado y UI refleja los cambios.
Consejos finales
- Mantén la lógica de negocio en el servidor (sanitización, validación, permisos).
- Evita cargar el bundle en todas las páginas de admin sólo en la tuya.
- Divide el store si tienes muchas entidades diferentes un store por dominio lógico suele ser más mantenible.
- Usa la arquitectura de controles/resolvers para separar las llamadas asíncronas de las acciones puras, facilitando testing.
Recursos útiles
Con esto tienes un tutorial completo y ejecutable para crear una pantalla de opciones en React usando @wordpress/data. Implementando y extendiendo los ejemplos que te he mostrado podrás adaptar la pantalla a cualquier conjunto de opciones y añadir funcionalidades avanzadas como validaciones, campos complejos y feedback en tiempo real.
|
Acepto donaciones de BAT's mediante el navegador Brave 🙂 |