Your client wants a layout the standard block editor cannot handle. You are stuck. Either you dump HTML into a 'Custom HTML' block — which breaks on the first update — or you build a custom Gutenberg block. We chose the second path, and not just because it looks cooler. Because a custom block built with React is the only way to give your WordPress the design freedom it needs to sell, without sacrificing performance or maintainability.
Why building custom Gutenberg blocks with React saves time and money?
Every time you use a page builder or a third-party block, you pay a subscription or accumulate technical debt. Custom blocks you own. At Meteora Web, we start from the client's P&L: a custom block costs once, then yields for years. With React inside Gutenberg you get reusable components, state management with ready hooks (useState, useEffect), and a control panel that feels like an app, not a 2000s form.
A concrete example: an e-commerce client needed a block to show discounted products with a countdown. With ACF and jQuery it was slow and untranslatable. We built a custom Gutenberg block in React: attributes for expiry date, countdown style, number of products. Development time: two days. Maintenance: zero. The site loaded 40% faster.
Static vs dynamic blocks
A static block saves HTML in post_content. Fast, but inflexible. A dynamic block (no save, only render_callback) generates output server-side. We use dynamic blocks for everything that must update without re-saving the post — e.g. latest articles, real-time prices. For our custom block, a JSX save gives you full control over output and works with visual recovery.
Sponsored Protocol
How to structure a custom Gutenberg block with React step by step?
Start with the folder. Inside your theme (or plugin) create blocks/block-name/. Inside put: block.json (the official WordPress declaration — mandatory since WP 6.0), edit.js (React component for the editor), save.js (frontend output), style.scss, index.js (entry point).
// block.json
{
"apiVersion": 3,
"name": "meteora/testimonial",
"title": "Testimonial",
"category": "widgets",
"icon": "format-quote",
"attributes": {
"quote": { "type": "string" },
"author": { "type": "string" },
"avatarURL": { "type": "string", "default": "" }
},
"supports": {
"align": ["wide", "full"],
"color": { "background": true, "text": true },
"spacing": { "padding": true }
},
"editorScript": "file:./index.js",
"style": "file:./style-index.css",
"render": "file:./render.php"
}
Note on render.php: if the block is dynamic, write the PHP markup here. We use it when data comes from the database or external APIs. For purely static blocks, keep the save.js and omit render.
Sponsored Protocol
Registering the block in the theme
The cleanest way: register_block_type( __DIR__ . '/blocks/testimonial' ); in functions.php. WordPress automatically loads block.json and React scripts. No manual enqueue.
// functions.php
add_action( 'init', function() {
register_block_type( get_template_directory() . '/blocks/testimonial' );
} );
How to write the Edit component and Save in React?
The Edit is a React component receiving props: attributes, setAttributes, clientId. Inside use @wordpress/components: RichText for content, MediaUpload for images, InspectorControls for sidebar settings.
// edit.js
import { __ } from '@wordpress/i18n';
import { useBlockProps, RichText, MediaUpload, InspectorControls } from '@wordpress/block-editor';
import { Button, PanelBody, TextControl } from '@wordpress/components';
export default function Edit({ attributes, setAttributes }) {
const { quote, author, avatarURL } = attributes;
return (
setAttributes({ avatarURL: media.url })}
render={({ open }) => (
)}
/>
{avatarURL &&
}
setAttributes({ quote: val })}
placeholder={__('Write the quote…', 'meteora')}
/>
setAttributes({ author: val })}
placeholder={__('Author', 'meteora')}
/>
);
}
The Save for a static block returns JSX that becomes the final HTML. Warning: do not use React components here — only plain markup.
Sponsored Protocol
// save.js
import { useBlockProps, RichText } from '@wordpress/block-editor';
export default function save({ attributes }) {
const { quote, author, avatarURL } = attributes;
return (
{avatarURL &&
}
{quote && {quote}
}
{author && {author}}
);
}
Common mistake: putting state logic or effects in save. Save must be purely declarative. If you need live data, use a dynamic block with render.php.
How to handle dynamic data and API requests inside a Gutenberg block?
Often the block must show data from a CPT, an external API, or a cache. In edit you use useSelect for WordPress queries on the client side (no server). Example: fetch the latest 5 posts.
Sponsored Protocol
import { useSelect } from '@wordpress/data';
import { useBlockProps } from '@wordpress/block-editor';
export default function LatestPostsEdit() {
const posts = useSelect((select) => {
return select('core').getEntityRecords('postType', 'post', {
per_page: 5,
_embed: true,
});
}, []);
return (
{posts ? (
{posts.map((post) => (
-
{post.title.raw}
))}
) : (
Loading…
)}
);
}
For requests to external services (e.g. price list from an ERP), better use the WordPress Store API and a custom endpoint in the theme. We have integrated blocks that read a catalog from external REST APIs — with server-side caching to avoid slowdowns.
How to test and optimize a custom Gutenberg block?
Tooling: @wordpress/scripts provides webpack preconfigured with JSX, SCSS, and minification. Run npm start for development and npm run build for production. We always add eslint and prettier for clean code.
Sponsored Protocol
Performance: avoid loading unnecessary React libraries. The WordPress core already includes React and wp-element. Your bundle should be small. Check with npm run build and verify file size.
Security: always sanitize attributes server-side. In render.php use esc_html, esc_url. Do not trust saved input. We see it often: custom blocks exposing XSS because text wasn't escaped.
Accessibility: use RichText which already handles aria-label and focus. Add role and aria-* where needed. Google doesn't penalise inaccessible blocks directly, but users do.
What to do now?
- Evaluate if you need a custom block. If the design requires unique interactions or live data, go for it. If you can manage with core blocks (Cover, Columns, Group) plus CSS, save time.
- Download @wordpress/create-block to scaffold. Command:
npx @wordpress/create-block meteora-testimonial. It gives you everything ready. - Read the official documentation at WordPress Block Editor Handbook.
- Check existing blocks — sometimes a third-party block can be extended with filters. We prefer to start from scratch for full control, but it's not always necessary.
- Contact us if you want us to develop custom blocks for your SME. We start from the problem, not the technology. See our Advanced WordPress Development page.