🎨 Sysprovider Code
Sysprovider LogoWiki
🇪🇸Hosting español para ecommerce

Desarrollo de Bloques Gutenberg Avanzados con React y TypeScript

Actualizado el 5 de junio de 2026

El ecosistema de WordPress ha evolucionado drásticamente desde la llegada del editor de bloques (Gutenberg). Lo que comenzó como una interfaz simple para crear contenido se ha convertido en un framework de desarrollo robusto. Para los desarrolladores que buscan llevar sus sitios al siguiente nivel, dominar el desarrollo de bloques Gutenberg avanzados con React y TypeScript ya no es una opción, sino una necesidad.

Este artículo es una guía técnica exhaustiva para crear bloques personalizados complejos, optimizados para el rendimiento y la mantenibilidad. Dejaremos atrás los ejemplos básicos de "Hola Mundo" y nos sumergiremos en patrones avanzados, tipado estricto y mejores prácticas de SysAdmin para el ecosistema WordPress.

La Base: Por qué React y TypeScript en Gutenberg

El editor de bloques de WordPress está construido sobre React. Cada bloque es, en esencia, un componente de React. Si bien se puede desarrollar un bloque con JavaScript vanilla, la complejidad de los bloques modernos (con múltiples atributos, paneles de control y lógica asíncrona) exige un enfoque más estructurado.

Ventajas de TypeScript en el Desarrollo de Bloques

  • Autocompletado y Documentación Viva: TypeScript entiende la estructura de los atributos del bloque, las propiedades de wp.components y los tipos de datos del store de WordPress.
  • Reducción de Errores en Tiempo de Compilación: Capturas errores de tipado antes de que lleguen al navegador del usuario. Esto es crítico en un entorno donde los hooks de React y las APIs de WordPress (wp.data, wp.apiFetch) se entrelazan.
  • Mantenibilidad a Largo Plazo: Un bloque con 10+ atributos y 5 componentes internos es un infierno sin tipos. TypeScript actúa como un contrato que define qué datos espera y retorna cada pieza.

[TIP] Si estás migrando un bloque de JavaScript a TypeScript, comienza tipando los atributos del bloque (attributes) y las propiedades de los componentes (Props). El resto del tipado fluirá naturalmente.

Configuración del Entorno de Desarrollo

Antes de escribir código, necesitamos un entorno que compile TypeScript y empaquete React. Olvídate de los scripts inline. Usaremos wp-scripts, la herramienta oficial de WordPress, pero con una configuración personalizada para TypeScript.

Estructura de Archivos Recomendada

mi-tema-o-plugin/
├── src/
│   ├── blocks/
│   │   └── mi-bloque-avanzado/
│   │       ├── block.json
│   │       ├── edit.tsx
│   │       ├── save.tsx
│   │       ├── editor.scss
│   │       ├── style.scss
│   │       └── index.ts
│   └── shared/
│       └── types.ts
├── build/
├── package.json
├── tsconfig.json
└── webpack.config.js (opcional, para sobrescribir defaults)

Configuración de tsconfig.json

Necesitas un tsconfig.json que apunte a JSX y a los tipos de WordPress.

{
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    "jsx": "react-jsx",
    "strict": true,
    "moduleResolution": "node",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "baseUrl": ".",
    "paths": {
      "@wordpress/*": ["node_modules/@wordpress/*"]
    }
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "build"]
}

Instalación de Dependencias Clave

npm init @wordpress/block --namespace mi-espacio
cd mi-bloque-avanzado
npm install --save-dev typescript @types/react @types/wordpress__blocks @types/wordpress__components

[WARNING] Asegúrate de que tu package.json tenga el script "build": "wp-scripts build" y "start": "wp-scripts start". La compilación de TypeScript se maneja automáticamente a través de Babel si usas wp-scripts.

Creando un Bloque Avanzado: El Editor de Lista con Arrastre

Vamos a construir un bloque que permita al usuario crear una lista de elementos con un título, descripción e imagen. Incluirá un panel de control lateral y soporte para arrastrar y soltar elementos (usando react-beautiful-dnd o similar). Este es un patrón común para testimonios, equipos o galerías de enlaces.

1. Definición del Bloque (block.json)

El archivo block.json es el corazón del bloque en la era de la API de Bloques v2. Aquí definimos atributos, soportes y estilos.

{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 3,
  "name": "mi-espacio/lista-avanzada",
  "title": "Lista Avanzada (React + TS)",
  "category": "widgets",
  "icon": "list-view",
  "description": "Bloque personalizado con lista de elementos arrastrables.",
  "attributes": {
    "items": {
      "type": "array",
      "default": [],
      "items": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "title": { "type": "string", "default": "" },
          "description": { "type": "string", "default": "" },
          "imageUrl": { "type": "string", "default": "" }
        }
      }
    },
    "columns": {
      "type": "number",
      "default": 1
    }
  },
  "supports": {
    "html": false,
    "align": true
  },
  "editorScript": "file:./index.js",
  "editorStyle": "file:./index.css",
  "style": "file:./style-index.css"
}

2. Tipos Compartidos (src/shared/types.ts)

Crear un archivo de tipos centralizado evita la duplicación y facilita el mantenimiento.

export interface ListItem {
  id: string;
  title: string;
  description: string;
  imageUrl: string;
}

export interface ListaAvanzadaAttributes {
  items: ListItem[];
  columns: number;
}

3. El Componente de Edición (edit.tsx)

Aquí es donde ocurre la magia. Usaremos hooks de React, el MediaUpload de WordPress y un pequeño sistema de arrastre.

import { __ } from '@wordpress/i18n';
import {
  useBlockProps,
  InspectorControls,
  MediaUpload,
  MediaUploadCheck,
} from '@wordpress/block-editor';
import {
  PanelBody,
  Button,
  TextControl,
  TextareaControl,
  RangeControl,
} from '@wordpress/components';
import { useState } from '@wordpress/element';
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
import type { ListItem, ListaAvanzadaAttributes } from '../../shared/types';

interface EditProps {
  attributes: ListaAvanzadaAttributes;
  setAttributes: (attrs: Partial<ListaAvanzadaAttributes>) => void;
}

export default function Edit({ attributes, setAttributes }: EditProps) {
  const { items, columns } = attributes;
  const blockProps = useBlockProps();

  const addItem = () => {
    const newItem: ListItem = {
      id: `item-${Date.now()}`,
      title: '',
      description: '',
      imageUrl: '',
    };
    setAttributes({ items: [...items, newItem] });
  };

  const updateItem = (index: number, key: keyof ListItem, value: string) => {
    const newItems = [...items];
    newItems[index][key] = value;
    setAttributes({ items: newItems });
  };

  const removeItem = (index: number) => {
    const newItems = items.filter((_, i) => i !== index);
    setAttributes({ items: newItems });
  };

  const onDragEnd = (result: any) => {
    if (!result.destination) return;
    const reordered = Array.from(items);
    const [removed] = reordered.splice(result.source.index, 1);
    reordered.splice(result.destination.index, 0, removed);
    setAttributes({ items: reordered });
  };

  return (
    <>
      <InspectorControls>
        <PanelBody title={__('Configuración de Lista', 'mi-texto')}>
          <RangeControl
            label={__('Columnas', 'mi-texto')}
            value={columns}
            onChange={(value) => setAttributes({ columns: value ?? 1 })}
            min={1}
            max={4}
          />
          <Button variant="primary" onClick={addItem}>
            {__('Añadir Elemento', 'mi-texto')}
          </Button>
        </PanelBody>
      </InspectorControls>

      <div {...blockProps}>
        <DragDropContext onDragEnd={onDragEnd}>
          <Droppable droppableId="list-items" direction="horizontal">
            {(provided) => (
              <div
                ref={provided.innerRef}
                {...provided.droppableProps}
                style={{
                  display: 'grid',
                  gridTemplateColumns: `repeat(${columns}, 1fr)`,
                  gap: '1rem',
                }}
              >
                {items.map((item, index) => (
                  <Draggable key={item.id} draggableId={item.id} index={index}>
                    {(provided) => (
                      <div
                        ref={provided.innerRef}
                        {...provided.draggableProps}
                        {...provided.dragHandleProps}
                        style={{
                          ...provided.draggableProps.style,
                          border: '1px solid #ccc',
                          padding: '1rem',
                          background: '#f9f9f9',
                        }}
                      >
                        <TextControl
                          label={__('Título', 'mi-texto')}
                          value={item.title}
                          onChange={(value) => updateItem(index, 'title', value)}
                        />
                        <TextareaControl
                          label={__('Descripción', 'mi-texto')}
                          value={item.description}
                          onChange={(value) => updateItem(index, 'description', value)}
                        />
                        <MediaUploadCheck>
                          <MediaUpload
                            onSelect={(media: any) =>
                              updateItem(index, 'imageUrl', media.url)
                            }
                            allowedTypes={['image']}
                            render={({ open }) => (
                              <Button onClick={open} variant="secondary">
                                {item.imageUrl
                                  ? __('Cambiar Imagen', 'mi-texto')
                                  : __('Seleccionar Imagen', 'mi-texto')}
                              </Button>
                            )}
                          />
                        </MediaUploadCheck>
                        {item.imageUrl && (
                          <img
                            src={item.imageUrl}
                            alt={item.title}
                            style={{ maxWidth: '100%', marginTop: '0.5rem' }}
                          />
                        )}
                        <Button
                          variant="link"
                          isDestructive
                          onClick={() => removeItem(index)}
                          style={{ marginTop: '0.5rem' }}
                        >
                          {__('Eliminar', 'mi-texto')}
                        </Button>
                      </div>
                    )}
                  </Draggable>
                ))}
                {provided.placeholder}
              </div>
            )}
          </Droppable>
        </DragDropContext>
      </div>
    </>
  );
}

[INFO] El uso de react-beautiful-dnd requiere que lo agregues como dependencia en tu package.json y que lo importes correctamente. wp-scripts maneja el bundle, pero debes asegurarte de que la librería esté disponible en el frontend.

4. El Componente de Guardado (save.tsx)

El componente save es lo que se renderiza en el frontend. Como es estático (sin React), generamos HTML limpio.

import { useBlockProps } from '@wordpress/block-editor';
import type { ListaAvanzadaAttributes } from '../../shared/types';

export default function save({ attributes }: { attributes: ListaAvanzadaAttributes }) {
  const { items, columns } = attributes;
  const blockProps = useBlockProps.save();

  return (
    <div {...blockProps}>
      {items.length === 0 && <p>No hay elementos en la lista.</p>}
      <div
        style={{
          display: 'grid',
          gridTemplateColumns: `repeat(${columns}, 1fr)`,
          gap: '1rem',
        }}
      >
        {items.map((item) => (
          <div key={item.id} className="lista-avanzada-item">
            {item.imageUrl && (
              
            )}
            <h3>{item.title}</h3>
            <p>{item.description}</p>
          </div>
        ))}
      </div>
    </div>
  );
}

5. El Punto de Entrada (index.ts)

Registramos el bloque y los componentes.

import { registerBlockType } from '@wordpress/blocks';
import metadata from './block.json';
import Edit from './edit';
import Save from './save';
import './editor.scss';
import './style.scss';

registerBlockType(metadata.name, {
  ...metadata,
  edit: Edit,
  save: Save,
});

Buenas Prácticas de SysAdmin y Rendimiento

Un bloque avanzado puede ser pesado. Como SysAdmin, debes asegurarte de que no degrade la experiencia del usuario ni del editor.

Optimización de Assets

  • Code Splitting: Si tu bloque usa librerías grandes (como react-beautiful-dnd), considera cargarlas solo en el editor. En block.json, usa viewScript y editorScript para separar la lógica.
  • Minificación: wp-scripts ya minifica en producción, pero verifica que no haya dependencias duplicadas.
  • Caché de Larga Duración: Nombra tus archivos con hash (lo hace wp-scripts por defecto). Configura tu servidor (Nginx/Apache) para cachear build/ durante 1 año.

Seguridad

  • Sanitización de Atributos: Nunca confíes en el contenido que viene del editor. Usa wp_kses_post en PHP si vas a procesar el HTML guardado.
  • Permisos: Los bloques personalizados deben respetar los permisos de usuario. Si tu bloque guarda datos en post meta, verifica current_user_can('edit_posts') en el backend.

Pruebas

  • TypeScript Compiler: Ejecuta npx tsc --noEmit para verificar errores de tipo sin generar archivos.
  • Linting: Usa @wordpress/eslint-plugin con reglas para React y TypeScript.
  • Pruebas Unitarias: Con Jest y @testing-library/react, puedes probar los componentes edit y save de forma aislada.

[WARNING] No subas la carpeta node_modules ni archivos fuente a producción. Usa un .gitignore estricto y despliega solo la carpeta build/ y el archivo block.json.

Integración con APIs Externas y el Data Store

Un bloque avanzado a menudo necesita datos dinámicos. TypeScript brilla aquí al tipar las respuestas de la API.

Ejemplo: Bloque que Muestra Posts Relacionados

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

interface Post {
  id: number;
  title: { rendered: string };
  link: string;
}

const RelatedPosts = () => {
  const posts: Post[] = useSelect((select) => {
    return select(coreStore).getEntityRecords('postType', 'post', {
      per_page: 5,
      _fields: 'id,title,link',
    }) as Post[];
  }, []);

  return (
    <ul>
      {posts?.map((post) => (
        <li key={post.id}>
          <a href={post.link}>{post.title.rendered}</a>
        </li>
      ))}
    </ul>
  );
};

TypeScript infiere que getEntityRecords devuelve Post[], lo que permite autocompletado en post.title.rendered.

Conclusión y Próximos Pasos

Desarrollar bloques Gutenberg avanzados con React y TypeScript no solo es posible, sino que es la forma más profesional y escalable de trabajar con WordPress. Has visto cómo configurar un

¿Necesitas ayuda?Son dos de nuestros técnicos, Agustín y Mikel, y están disponibles para resolver cualquier problema.

Hablar con ellos ahora
Agustín y Mikel