За 16 лет работы с WordPress создал более 150 кастомных блоков — от простых карточек до сложных систем с real-time данными. Первые блоки писал на jQuery, переходил на Backbone, потом на React. В 2025 году разработка Gutenberg блоков достигла зрелости — инструментарий мощный, API стабильные, но документация все еще оставляет желать лучшего. Сегодня разберу продвинутые техники, которые редко описаны в официальных гайдах.
Динамические блоки: серверный vs клиентский рендеринг
Когда нужен server-side rendering
На проекте новостного портала создавал блок «Популярные статьи». Первая реализация через REST API в редакторе работала, но генерировала 3 дополнительных запроса на каждое открытие поста для редактирования. Миграция на серверный рендеринг сократила время загрузки редактора с 4.2 до 1.8 секунды.
SSR оправдан когда:
- Данные меняются часто (статистика, погода, курсы валют)
- Нужен доступ к данным, недоступным через REST API
- Требуются сложные WP_Query с учетом прав пользователя
- Критична безопасность (скрытие логики от клиента)
Правильная реализация динамического блока
block.json с render callback:
json<code>{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "mytheme/popular-posts",
"title": "Popular Posts",
"category": "widgets",
"icon": "chart-bar",
"attributes": {
"numberOfPosts": {
"type": "number",
"default": 5
},
"category": {
"type": "string",
"default": ""
},
"timeRange": {
"type": "string",
"default": "week",
"enum": ["day", "week", "month", "year", "all"]
},
"showExcerpt": {
"type": "boolean",
"default": true
},
"showThumbnail": {
"type": "boolean",
"default": true
}
},
"textdomain": "mytheme",
"editorScript": "file:./index.js",
"editorStyle": "file:./index.css",
"style": "file:./style-index.css",
"render": "file:./render.php"
}
</code>render.php — серверная логика:
php<code><?php
<em>/**
</em><em> * @var array $attributes Block attributes
</em><em> * @var string $content Block default content
</em><em> * @var WP_Block $block Block instance
</em><em> */</em>
$number_of_posts = isset($attributes['numberOfPosts']) ? $attributes['numberOfPosts'] : 5;
$category = isset($attributes['category']) ? $attributes['category'] : '';
$time_range = isset($attributes['timeRange']) ? $attributes['timeRange'] : 'week';
$show_excerpt = isset($attributes['showExcerpt']) ? $attributes['showExcerpt'] : true;
$show_thumbnail = isset($attributes['showThumbnail']) ? $attributes['showThumbnail'] : true;
<em>// Определение временного диапазона</em>
$date_query = array();
switch ($time_range) {
case 'day':
$date_query = array('after' => '1 day ago');
break;
case 'week':
$date_query = array('after' => '1 week ago');
break;
case 'month':
$date_query = array('after' => '1 month ago');
break;
case 'year':
$date_query = array('after' => '1 year ago');
break;
}
$args = array(
'posts_per_page' => $number_of_posts,
'post_status' => 'publish',
'orderby' => 'meta_value_num',
'meta_key' => 'post_views_count',
'order' => 'DESC',
'ignore_sticky_posts' => true,
);
if (!empty($category)) {
$args['category_name'] = $category;
}
if (!empty($date_query)) {
$args['date_query'] = array($date_query);
}
<em>// Кэширование запроса</em>
$cache_key = 'popular_posts_' . md5(serialize($args));
$posts = wp_cache_get($cache_key);
if (false === $posts) {
$query = new WP_Query($args);
$posts = $query->posts;
wp_cache_set($cache_key, $posts, '', 3600); <em>// 1 час</em>
}
if (empty($posts)) {
return '<div class="popular-posts-empty">No popular posts found.</div>';
}
$wrapper_attributes = get_block_wrapper_attributes(array(
'class' => 'popular-posts-block'
));
?>
<div <?php echo $wrapper_attributes; ?>>
<ul class="popular-posts-list">
<?php foreach ($posts as $post): ?>
<li class="popular-post-item">
<?php if ($show_thumbnail && has_post_thumbnail($post->ID)): ?>
<div class="popular-post-thumbnail">
<a href="<?php echo get_permalink($post->ID); ?>">
<?php echo get_the_post_thumbnail($post->ID, 'thumbnail'); ?>
</a>
</div>
<?php endif; ?>
<div class="popular-post-content">
<h3 class="popular-post-title">
<a href="<?php echo get_permalink($post->ID); ?>">
<?php echo esc_html($post->post_title); ?>
</a>
</h3>
<?php if ($show_excerpt): ?>
<div class="popular-post-excerpt">
<?php echo wp_trim_words(get_the_excerpt($post->ID), 20); ?>
</div>
<?php endif; ?>
<div class="popular-post-meta">
<span class="post-views">
<?php echo number_format_i18n(get_post_meta($post->ID, 'post_views_count', true)); ?> views
</span>
<span class="post-date">
<?php echo human_time_diff(get_the_time('U', $post->ID), current_time('timestamp')); ?> ago
</span>
</div>
</div>
</li>
<?php endforeach; ?>
</ul>
</div>
</code>Edit компонент с ServerSideRender:
jsx<code>import { __ } from '@wordpress/i18n';
import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
import {
PanelBody,
RangeControl,
SelectControl,
ToggleControl,
Spinner
} from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import ServerSideRender from '@wordpress/server-side-render';
export default function Edit({ attributes, setAttributes }) {
const blockProps = useBlockProps();
<em>// Получение списка категорий</em>
const categories = useSelect((select) => {
return select('core').getEntityRecords('taxonomy', 'category', {
per_page: -1,
orderby: 'name',
order: 'asc'
});
}, []);
const categoryOptions = [
{ label: __('All Categories', 'mytheme'), value: '' },
...(categories || []).map((cat) => ({
label: cat.name,
value: cat.slug
}))
];
return (
<>
<InspectorControls>
<PanelBody title={__('Settings', 'mytheme')}>
<RangeControl
label={__('Number of Posts', 'mytheme')}
value={attributes.numberOfPosts}
onChange={(value) => setAttributes({ numberOfPosts: value })}
min={1}
max={20}
/>
<SelectControl
label={__('Category', 'mytheme')}
value={attributes.category}
options={categoryOptions}
onChange={(value) => setAttributes({ category: value })}
/>
<SelectControl
label={__('Time Range', 'mytheme')}
value={attributes.timeRange}
options={[
{ label: __('Last 24 Hours', 'mytheme'), value: 'day' },
{ label: __('Last Week', 'mytheme'), value: 'week' },
{ label: __('Last Month', 'mytheme'), value: 'month' },
{ label: __('Last Year', 'mytheme'), value: 'year' },
{ label: __('All Time', 'mytheme'), value: 'all' }
]}
onChange={(value) => setAttributes({ timeRange: value })}
/>
<ToggleControl
label={__('Show Excerpt', 'mytheme')}
checked={attributes.showExcerpt}
onChange={(value) => setAttributes({ showExcerpt: value })}
/>
<ToggleControl
label={__('Show Thumbnail', 'mytheme')}
checked={attributes.showThumbnail}
onChange={(value) => setAttributes({ showThumbnail: value })}
/>
</PanelBody>
</InspectorControls>
<div {...blockProps}>
<ServerSideRender
block="mytheme/popular-posts"
attributes={attributes}
LoadingResponsePlaceholder={() => (
<div className="loading-placeholder">
<Spinner />
<p>{__('Loading popular posts...', 'mytheme')}</p>
</div>
)}
ErrorResponsePlaceholder={({ response }) => (
<div className="error-placeholder">
<p>{__('Error loading posts. Please check your settings.', 'mytheme')}</p>
{response?.message && <small>{response.message}</small>}
</div>
)}
EmptyResponsePlaceholder={() => (
<div className="empty-placeholder">
<p>{__('No popular posts found.', 'mytheme')}</p>
</div>
)}
/>
</div>
</>
);
}
</code>Оптимизация ServerSideRender
Стандартный компонент ServerSideRender имеет проблему — при каждом изменении атрибута показывает spinner, что раздражает. Решение через debounce:
jsx<code>import { useState, useEffect, useCallback } from '@wordpress/element';
import { debounce } from '@wordpress/compose';
import ServerSideRender from '@wordpress/server-side-render';
function OptimizedServerSideRender({ block, attributes, ...props }) {
const [debouncedAttributes, setDebouncedAttributes] = useState(attributes);
<em>// Debounce на 300ms</em>
const updateAttributes = useCallback(
debounce((newAttrs) => {
setDebouncedAttributes(newAttrs);
}, 300),
[]
);
useEffect(() => {
updateAttributes(attributes);
}, [attributes, updateAttributes]);
return (
<ServerSideRender
block={block}
attributes={debouncedAttributes}
{...props}
/>
);
}
</code>useSelect и useDispatch: управление состоянием
Получение данных из WordPress stores
Самый мощный инструмент для работы с данными в Gutenberg — хуки useSelect и useDispatch:
jsx<code>import { useSelect, useDispatch } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';
import { store as editorStore } from '@wordpress/editor';
import { store as blockEditorStore } from '@wordpress/block-editor';
function AdvancedBlockEdit({ attributes, setAttributes, clientId }) {
<em>// Получение данных из нескольких stores одновременно</em>
const {
post,
postType,
categories,
media,
blocks,
isBlockSelected
} = useSelect((select) => {
const { getCurrentPost, getCurrentPostType } = select(editorStore);
const { getEntityRecords, getMedia } = select(coreStore);
const { getBlocks, isBlockSelected: checkSelected } = select(blockEditorStore);
return {
post: getCurrentPost(),
postType: getCurrentPostType(),
categories: getEntityRecords('taxonomy', 'category', { per_page: -1 }),
media: attributes.mediaId ? getMedia(attributes.mediaId) : null,
blocks: getBlocks(),
isBlockSelected: checkSelected(clientId)
};
}, [attributes.mediaId, clientId]);
<em>// Диспатч действий</em>
const { editPost } = useDispatch(editorStore);
const { insertBlocks, removeBlock } = useDispatch(blockEditorStore);
<em>// Пример: обновление мета-поля поста</em>
const updatePostMeta = (key, value) => {
editPost({
meta: {
[key]: value
}
});
};
<em>// Пример: программная вставка блока</em>
const insertRelatedContentBlock = () => {
const newBlock = createBlock('mytheme/related-content', {
postId: post.id
});
<em>// Вставка после текущего блока</em>
const rootBlocks = blocks;
const currentIndex = rootBlocks.findIndex(block => block.clientId === clientId);
insertBlocks(newBlock, currentIndex + 1);
};
<em>// ... остальная логика блока</em>
}
</code>Продвинутый пример: синхронизация с custom post meta
jsx<code>import { useSelect, useDispatch } from '@wordpress/data';
import { useEffect } from '@wordpress/element';
import { store as editorStore } from '@wordpress/editor';
function MetaSyncedBlock({ attributes, setAttributes }) {
const { meta, isSaving } = useSelect((select) => {
const editor = select(editorStore);
return {
meta: editor.getEditedPostAttribute('meta'),
isSaving: editor.isSavingPost()
};
}, []);
const { editPost } = useDispatch(editorStore);
<em>// Синхронизация атрибутов блока с мета-полями</em>
useEffect(() => {
if (meta && meta.custom_title !== attributes.title) {
setAttributes({ title: meta.custom_title || '' });
}
}, [meta, attributes.title, setAttributes]);
const handleTitleChange = (newTitle) => {
<em>// Обновляем и атрибут блока, и мета-поле</em>
setAttributes({ title: newTitle });
editPost({
meta: {
custom_title: newTitle
}
});
};
return (
<div className="meta-synced-block">
<TextControl
label="Title (synced with post meta)"
value={attributes.title}
onChange={handleTitleChange}
disabled={isSaving}
/>
{isSaving && <Spinner />}
</div>
);
}
</code>Block Context: передача данных между блоками
Создание родительского блока с контекстом
Block Context API позволяет передавать данные от родительского блока к дочерним без prop drilling:
Parent block (Container):
json<code><em>// block.json</em>
{
"name": "mytheme/product-container",
"title": "Product Container",
"category": "widgets",
"attributes": {
"productId": {
"type": "number"
},
"showPricing": {
"type": "boolean",
"default": true
},
"currency": {
"type": "string",
"default": "USD"
}
},
"providesContext": {
"mytheme/productId": "productId",
"mytheme/showPricing": "showPricing",
"mytheme/currency": "currency"
},
"supports": {
"html": false
}
}
</code>jsx<code><em>// edit.js</em>
import { InnerBlocks, useBlockProps, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, SelectControl, ToggleControl } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';
const ALLOWED_BLOCKS = [
'mytheme/product-title',
'mytheme/product-image',
'mytheme/product-price',
'mytheme/product-description'
];
const TEMPLATE = [
['mytheme/product-image'],
['mytheme/product-title'],
['mytheme/product-price'],
['mytheme/product-description']
];
export default function Edit({ attributes, setAttributes }) {
const blockProps = useBlockProps();
<em>// Получение списка продуктов</em>
const products = useSelect((select) => {
return select(coreStore).getEntityRecords('postType', 'product', {
per_page: -1
});
}, []);
const productOptions = products ? [
{ label: 'Select a product', value: 0 },
...products.map(product => ({
label: product.title.rendered,
value: product.id
}))
] : [];
return (
<>
<InspectorControls>
<PanelBody title="Product Settings">
<SelectControl
label="Product"
value={attributes.productId}
options={productOptions}
onChange={(productId) => setAttributes({
productId: parseInt(productId)
})}
/>
<ToggleControl
label="Show Pricing"
checked={attributes.showPricing}
onChange={(showPricing) => setAttributes({ showPricing })}
/>
<SelectControl
label="Currency"
value={attributes.currency}
options={[
{ label: 'USD', value: 'USD' },
{ label: 'EUR', value: 'EUR' },
{ label: 'GBP', value: 'GBP' }
]}
onChange={(currency) => setAttributes({ currency })}
/>
</PanelBody>
</InspectorControls>
<div {...blockProps}>
{attributes.productId === 0 ? (
<div className="product-container-placeholder">
<p>Please select a product from the sidebar.</p>
</div>
) : (
<InnerBlocks
allowedBlocks={ALLOWED_BLOCKS}
template={TEMPLATE}
templateLock="all"
/>
)}
</div>
</>
);
}
</code>Дочерний блок, использующий контекст
Child block (Product Price):
json<code><em>// block.json</em>
{
"name": "mytheme/product-price",
"title": "Product Price",
"category": "widgets",
"parent": ["mytheme/product-container"],
"usesContext": [
"mytheme/productId",
"mytheme/showPricing",
"mytheme/currency"
],
"attributes": {
"showLabel": {
"type": "boolean",
"default": true
}
}
}
</code>jsx<code><em>// edit.js</em>
import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, ToggleControl, Spinner } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';
export default function Edit({ attributes, setAttributes, context }) {
const blockProps = useBlockProps();
<em>// Получение данных продукта из контекста</em>
const productId = context['mytheme/productId'];
const showPricing = context['mytheme/showPricing'];
const currency = context['mytheme/currency'];
<em>// Загрузка продукта</em>
const { product, isResolving } = useSelect((select) => {
const { getEntityRecord, isResolving: checkResolving } = select(coreStore);
return {
product: productId ? getEntityRecord('postType', 'product', productId) : null,
isResolving: checkResolving('getEntityRecord', ['postType', 'product', productId])
};
}, [productId]);
if (!showPricing) {
return (
<div {...blockProps}>
<p className="pricing-hidden-notice">
Pricing is hidden by parent block settings
</p>
</div>
);
}
if (isResolving) {
return (
<div {...blockProps}>
<Spinner />
</div>
);
}
if (!product) {
return (
<div {...blockProps}>
<p className="no-product-notice">No product selected</p>
</div>
);
}
const price = product.meta?.price || 0;
const formattedPrice = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: currency
}).format(price);
return (
<>
<InspectorControls>
<PanelBody title="Price Display">
<ToggleControl
label="Show Label"
checked={attributes.showLabel}
onChange={(showLabel) => setAttributes({ showLabel })}
/>
</PanelBody>
</InspectorControls>
<div {...blockProps}>
<div className="product-price">
{attributes.showLabel && (
<span className="price-label">Price: </span>
)}
<span className="price-amount">{formattedPrice}</span>
</div>
</div>
</>
);
}
</code>InnerBlocks: вложенные структуры
Продвинутые паттерны с InnerBlocks
На проекте лендинга создавал блок «Feature Grid» с динамическим количеством колонок:
jsx<code>import { InnerBlocks, useBlockProps, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, RangeControl, SelectControl } from '@wordpress/components';
import { useSelect, useDispatch } from '@wordpress/data';
import { store as blockEditorStore } from '@wordpress/block-editor';
import { createBlock } from '@wordpress/blocks';
import { useEffect } from '@wordpress/element';
export default function Edit({ attributes, setAttributes, clientId }) {
const blockProps = useBlockProps({
className: `columns-${attributes.columns}`
});
<em>// Получение текущих innerBlocks</em>
const { innerBlocks } = useSelect((select) => {
return {
innerBlocks: select(blockEditorStore).getBlocks(clientId)
};
}, [clientId]);
const { replaceInnerBlocks } = useDispatch(blockEditorStore);
<em>// Автоматическое добавление/удаление колонок при изменении их количества</em>
useEffect(() => {
const currentCount = innerBlocks.length;
const desiredCount = attributes.columns;
if (currentCount === desiredCount) return;
let newBlocks = [...innerBlocks];
if (currentCount < desiredCount) {
<em>// Добавляем недостающие блоки</em>
const blocksToAdd = desiredCount - currentCount;
for (let i = 0; i < blocksToAdd; i++) {
newBlocks.push(createBlock('mytheme/feature-item'));
}
} else {
<em>// Удаляем лишние блоки</em>
newBlocks = newBlocks.slice(0, desiredCount);
}
replaceInnerBlocks(clientId, newBlocks);
}, [attributes.columns, innerBlocks.length, clientId]);
const ALLOWED_BLOCKS = ['mytheme/feature-item'];
return (
<>
<InspectorControls>
<PanelBody title="Grid Settings">
<RangeControl
label="Number of Columns"
value={attributes.columns}
onChange={(columns) => setAttributes({ columns })}
min={1}
max={6}
/>
<SelectControl
label="Gap Size"
value={attributes.gap}
options={[
{ label: 'Small', value: 'small' },
{ label: 'Medium', value: 'medium' },
{ label: 'Large', value: 'large' }
]}
onChange={(gap) => setAttributes({ gap })}
/>
</PanelBody>
</InspectorControls>
<div {...blockProps}>
<InnerBlocks
allowedBlocks={ALLOWED_BLOCKS}
orientation="horizontal"
renderAppender={false}
/>
</div>
</>
);
}
</code>Block Variations: создание пресетов
Динамические вариации блока
Block Variations позволяют создавать пресконфигурированные версии блока:
js<code>import { registerBlockVariation } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
<em>// Регистрация вариаций для блока кнопки</em>
registerBlockVariation('core/button', {
name: 'cta-primary',
title: __('Primary CTA', 'mytheme'),
description: __('Primary call-to-action button', 'mytheme'),
icon: 'megaphone',
attributes: {
text: __('Get Started', 'mytheme'),
className: 'is-style-primary-cta',
backgroundColor: 'primary',
textColor: 'white',
fontSize: 'large',
borderRadius: 50
},
scope: ['inserter', 'transform']
});
registerBlockVariation('core/button', {
name: 'cta-secondary',
title: __('Secondary CTA', 'mytheme'),
attributes: {
text: __('Learn More', 'mytheme'),
className: 'is-style-secondary-cta',
style: {
border: {
width: '2px'
}
}
}
});
<em>// Вариации с innerBlocks</em>
registerBlockVariation('core/group', {
name: 'hero-section',
title: __('Hero Section', 'mytheme'),
description: __('Pre-configured hero section layout', 'mytheme'),
icon: 'cover-image',
attributes: {
align: 'full',
className: 'hero-section',
style: {
spacing: {
padding: {
top: '80px',
bottom: '80px'
}
}
}
},
innerBlocks: [
['core/heading', {
level: 1,
content: __('Welcome to Our Site', 'mytheme'),
textAlign: 'center'
}],
['core/paragraph', {
content: __('Your journey starts here', 'mytheme'),
align: 'center',
fontSize: 'large'
}],
['core/buttons', {
layout: { type: 'flex', justifyContent: 'center' }
}, [
['core/button', {
text: __('Get Started', 'mytheme')
}]
]]
],
scope: ['inserter']
});
</code>Interactivity API: интерактивные блоки
Современный подход к интерактивности
В WordPress 6.5+ появился Interactivity API — декларативный подход к созданию интерактивных блоков без jQuery:
view.js — логика взаимодействия:
js<code>import { store, getContext } from '@wordpress/interactivity';
const { state } = store('mytheme/accordion', {
state: {
get isAnyOpen() {
return state.items.some(item => item.isOpen);
},
items: []
},
actions: {
toggle: () => {
const context = getContext();
context.isOpen = !context.isOpen;
<em>// Закрытие других элементов (accordion behavior)</em>
if (context.isOpen && context.closeOthers) {
state.items.forEach((item, index) => {
if (index !== context.index) {
item.isOpen = false;
}
});
}
},
openAll: () => {
state.items.forEach(item => {
item.isOpen = true;
});
},
closeAll: () => {
state.items.forEach(item => {
item.isOpen = false;
});
}
},
callbacks: {
logToggle: () => {
const context = getContext();
console.log(`Item ${context.index} toggled:`, context.isOpen);
}
}
});
</code>render.php — разметка с директивами:
php<code><?php
$unique_id = wp_unique_id('accordion-');
$items = isset($attributes['items']) ? $attributes['items'] : array();
<em>// Инициализация состояния</em>
wp_interactivity_state('mytheme/accordion', array(
'items' => array_map(function($item, $index) {
return array(
'isOpen' => false,
'index' => $index
);
}, $items, array_keys($items))
));
$wrapper_attributes = get_block_wrapper_attributes(array(
'class' => 'accordion-block',
'data-wp-interactive' => 'mytheme/accordion'
));
?>
<div <?php echo $wrapper_attributes; ?>>
<div class="accordion-controls">
<button
data-wp-on--click="actions.openAll"
class="accordion-control-btn"
>
<?php _e('Open All', 'mytheme'); ?>
</button>
<button
data-wp-on--click="actions.closeAll"
class="accordion-control-btn"
>
<?php _e('Close All', 'mytheme'); ?>
</button>
</div>
<div class="accordion-items">
<?php foreach ($items as $index => $item): ?>
<div
class="accordion-item"
data-wp-context='<?php echo wp_json_encode(array(
'isOpen' => false,
'index' => $index,
'closeOthers' => $attributes['closeOthers'] ?? true
)); ?>'
>
<button
class="accordion-header"
data-wp-on--click="actions.toggle"
data-wp-on-async--click="callbacks.logToggle"
data-wp-class--is-open="context.isOpen"
aria-expanded="false"
data-wp-bind--aria-expanded="context.isOpen"
>
<span><?php echo esc_html($item['title']); ?></span>
<svg
class="accordion-icon"
data-wp-class--rotated="context.isOpen"
width="20"
height="20"
viewBox="0 0 20 20"
>
<path d="M5 7.5l5 5 5-5" stroke="currentColor" />
</svg>
</button>
<div
class="accordion-content"
data-wp-bind--hidden="!context.isOpen"
data-wp-class--is-visible="context.isOpen"
>
<div class="accordion-content-inner">
<?php echo wp_kses_post($item['content']); ?>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</code>Продвинутая типизация с TypeScript
Typed блок с полной type-safety
typescript<code><em>// types.ts</em>
import { BlockEditProps } from '@wordpress/blocks';
export interface ProductAttributes {
productId: number;
showPrice: boolean;
showDescription: boolean;
layout: 'grid' | 'list';
columns: number;
}
export interface Product {
id: number;
title: {
rendered: string;
};
content: {
rendered: string;
};
meta: {
price: number;
sku: string;
stock: number;
};
featured_media: number;
}
export type ProductBlockEditProps = BlockEditProps<ProductAttributes>;
<em>// edit.tsx</em>
import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, RangeControl, ToggleControl } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';
import type { ProductBlockEditProps, Product } from './types';
export default function Edit({
attributes,
setAttributes
}: ProductBlockEditProps): JSX.Element {
const blockProps = useBlockProps();
const product = useSelect<Product | null>(
(select) => {
if (!attributes.productId) return null;
return select(coreStore).getEntityRecord<Product>(
'postType',
'product',
attributes.productId
);
},
[attributes.productId]
);
const handleColumnsChange = (columns: number): void => {
setAttributes({ columns });
};
return (
<>
<InspectorControls>
<PanelBody title="Settings">
<RangeControl
label="Columns"
value={attributes.columns}
onChange={handleColumnsChange}
min={1}
max={4}
/>
<ToggleControl
label="Show Price"
checked={attributes.showPrice}
onChange={(showPrice: boolean) => setAttributes({ showPrice })}
/>
</PanelBody>
</InspectorControls>
<div {...blockProps}>
{product && (
<div className="product-display">
<h3>{product.title.rendered}</h3>
{attributes.showPrice && (
<span className="price">${product.meta.price}</span>
)}
</div>
)}
</div>
</>
);
}
</code>Продвинутая разработка Gutenberg блоков в 2025 — это симбиоз React, WordPress API и глубокого понимания архитектуры редактора. Динамические блоки с серверным рендерингом решают проблемы производительности, useSelect/useDispatch дают полный контроль над данными, Block Context элегантно передает информацию между блоками, а Interactivity API делает блоки по-настоящему интерактивными без костылей. Начинайте с простых статических блоков, постепенно добавляйте динамику, изучайте TypeScript для type-safety, используйте Interactivity API для фронтенда. Каждый проект — это шаг к пониманию всей мощи современного Gutenberg.



