Desarrollo de Bloques Gutenberg Avanzados con React y TypeScript
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.componentsy 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.jsontenga 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 usaswp-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-dndrequiere que lo agregues como dependencia en tupackage.jsony que lo importes correctamente.wp-scriptsmaneja 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. Enblock.json, usaviewScriptyeditorScriptpara separar la lógica. - Minificación:
wp-scriptsya minifica en producción, pero verifica que no haya dependencias duplicadas. - Caché de Larga Duración: Nombra tus archivos con hash (lo hace
wp-scriptspor defecto). Configura tu servidor (Nginx/Apache) para cachearbuild/durante 1 año.
Seguridad
- Sanitización de Atributos: Nunca confÃes en el contenido que viene del editor. Usa
wp_kses_posten 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 --noEmitpara verificar errores de tipo sin generar archivos. - Linting: Usa
@wordpress/eslint-plugincon reglas para React y TypeScript. - Pruebas Unitarias: Con Jest y
@testing-library/react, puedes probar los componenteseditysavede forma aislada.
[WARNING] No subas la carpeta
node_modulesni archivos fuente a producción. Usa un.gitignoreestricto y despliega solo la carpetabuild/y el archivoblock.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
