...

Кастомные Gutenberg блоки: продвинутая разработка с React

24 октября 2025
Кастомные Gutenberg блоки: продвинутая разработка с React

За 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.

Хотите узнать стоимость сайта?

Обсудим проект и рассчитаем стомость вашего сайта

    Нажимая на кнопку, вы даете согласие на обработку своих персональных данных

    This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.

    Ваша заявка принята!

    Мы перезвоним вам в ближайшее время.