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 🙂 |
