Como crear una pantalla de opciones en React con @wordpress/data (JS) en WordPress

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:

  1. 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:

  1. PHP registra la página admin, encola el JS y expone rutas REST seguras.
  2. JS configura apiFetch con el nonce para autenticar llamadas REST.
  3. Se registra un store con @wordpress/data que define acciones, reducers y controls para llamadas asíncronas.
  4. El componente React usa useSelect para leer el estado y useDispatch para disparar acciones (fetch/update).
  5. 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 🙂



Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *