Blocks have been around for a while now, and we’ve gotten to the point where we made our own blocks and also integrated with blocks from other plugins.
Some of them have good standards that made our lives easy, and others… not so much.
Two great examples are GiveWP and The Events Calendar.
Both are great products for their purposes, but when it comes to 3rd party developer experience for blocks, they are in opposite corners.
#The WordPress Way
In WordPress, everything is meant to be extendable through hooks. Both in PHP and in JavaScript.
For PHP, they have hook functions, and for JavaScript, they offer an NPM package called @wordpress/hooks.
All of that is super convenient for developers working on a product and for 3rd parties that are looking to extend said product.
And that’s one of the reasons WordPress is so successful and great to work with, because the codebase is open to the community.
That being said, hooks and filters are never enough. The entire UI of your plugin should be extendable, so that other developers can add settings and UI elements to extend your functionality.
And your internal logic/mechanics should also be extendable, so that said features can run and sync with your own features to benefit the end users.
#Data Structures
Knowing that you are building for WordPress you should always do your best to use the ecosystem’s mechanisms to store and handle data or state.
You should use mechanisms like Options API, Post Types, and Transients to store data, and you should use Block Attributes to store your block’s state.
That way, your data is easy to work with and allows us and other 3rd party developers to extend your product and benefit your users.
#GiveWP
Working with GiveWP was straightforward; they have a custom React setup for their admin panel, and they have blocks.
Their code is highly extensible through hooks, it is available on Github, and their developer documentation is great!
In our case, we had to add changes to their plugin settings and to their blocks’ edit state. We’ve included real code samples below.
#Real World Example
GiveWP uses a custom post type with predefined and locked blocks, and their donation forms look something like this:
As you can see, that looks like any other post you would edit through the Block Builder, with the sidebar menu on the right and the preview in the middle.
And the way you interact with the block settings is by following the core WordPress blocks — everything is happening through the sidebar on the right.
For us developers, that means that we can use the default edit state filters to extend the sidebar settings and add our own. While at the same time, use the block attributes filters to add/modify block attributes.
Their saving mechanism is the default mechanism that any post uses, so it is completely extendable. That way, we can catch the block data in the backend and save our own settings, which we can later use for our features.
Everything allows us to extend their blocks with this code snippet:
addFilter(
'editor.BlockEdit',
'wp-fusion/wp-fusion-donation-extension',
createHigherOrderComponent( ( BlockEdit ) => {
return ( props ) => {
if ( props.name !== 'givewp/donation-amount' ) {
return <BlockEdit { ...props } />;
}
return (
<>
<BlockEdit { ...props } />
<ExtendDonationAmount { ...props } />
</>
);
};
}, 'withInspectorControls' )
);
That will extend the edit state of the donation amount block and will add a panel with our own settings, which are in the ExtendDonationAmount component.
That easy, a few lines of code and you have your UI extension.
#The Events Calendar
The Events Calendar is different. Their code is available on Github, but their developer documentation is not that great. Or at least, that’s how it is at the time of writing.
Their React-based code is not that extensible, because they don’t offer hooks that allow 3rd party developers to extend their blocks, and also, instead of using the default WordPress state stores through block attributes, they have their own state management and even a custom saving mechanism.
#Real World Example
On the opposite corner compared to GiveWP, The Events Calendar uses custom mechanisms for everything. They don’t store their state in the block attributes, and they don’t save their data to the database when the post is saved.
This is how editing an event ticket (as a block) looks on their end:
As you can see, the only settings that you would normally have in the sidebar with any WordPress block are ours. Their whole form is within the block, and it can be saved only by pressing the Create Ticket button.
The form data is not stored in block attributes; it is stored using a custom mechanism that is not related in any way to the traditional WordPress ways.
And the worst part is that there aren’t any hooks that would allow us to extend the UI of the block or the saving process.
So we had to get creative and extend the edit state as we did for GiveWP and then add a listener to the Save Post button so that when the user saves the post, we send a custom API call through @wordpress/api-fetch so that we can add our settings to the database.
To get to that solution, we had to spend a lot of time researching the way the plugin works and to make sure we had no other solution than to do creative thinking. The resulting code is much more complex (and likely to break).
As an example, take a look at WP Fusion’s integration with Event Ticket’s ticket editing functionality (which doesn’t even account for RSVPs):
The save functionality:
import { createHigherOrderComponent } from '@wordpress/compose';
import { subscribe, select } from '@wordpress/data';
import { TicketSettings } from '../partials/TicketSettings';
// We don't use state here because we're using the subscribe function to check if the post is being saved.
// If we used state, the component would re-render and the save function would be called again, which would cause an infinite loop.
let saveCounter = 0; // Track save operations
let isSaveInProgress = false;
export const handleTicketsFilter = createHigherOrderComponent(
( BlockEdit ) => {
return ( props ) => {
if ( props.name !== 'tribe/tickets-item' ) {
return <BlockEdit { ...props } />;
}
subscribe( () => {
const isSaving = select( 'core/editor' ).isSavingPost();
const isAutosaving = select( 'core/editor' ).isAutosavingPost();
if ( isSaving && ! isAutosaving && ! isSaveInProgress ) {
const currentSaveCounter = ++saveCounter; // Increment and capture current counter
isSaveInProgress = true;
if (
window.wpfTribeTickets &&
window.wpfTribeTickets.saveTicketsSettings
) {
window.wpfTribeTickets.saveTicketsSettings();
const unsubscribe = subscribe( () => {
const didSucceed =
select(
'core/editor'
).didPostSaveRequestSucceed();
const didFail =
select(
'core/editor'
).didPostSaveRequestFail();
if (
( didSucceed || didFail ) &&
saveCounter === currentSaveCounter
) {
isSaveInProgress = false;
unsubscribe();
}
} );
}
}
} );
return (
<>
<BlockEdit { ...props } />
<TicketSettings { ...props } />
</>
);
};
},
'withInspectorControls'
);
The ticket settings partial:
import { InspectorControls } from '@wordpress/block-editor';
import { useState, useCallback, useEffect, useRef } from '@wordpress/element';
import apiFetch from '@wordpress/api-fetch';
import { PanelBody } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { SettingsPanel } from './SettingsPanel';
const ticketSettings = window.wpfTribeTickets.tickets;
export const TicketSettings = ( { attributes } ) => {
const [ error, setError ] = useState( '' );
const [ tagValues, setTagValues ] = useState( {
applyTags: [],
applyDeletedTags: [],
applyCheckInTags: [],
addAttendees: false,
} );
const saveRequestInProgress = useRef( false );
const lastSavedData = useRef( null );
const { ticketId } = attributes;
const isDisabled = ticketId === 0;
const ticketData = ticketId
? ticketSettings.find( ( ticket ) => ticket[ ticketId ] )
: false;
useEffect( () => {
if ( ! ticketData ) {
return;
}
const settings = ticketData[ ticketId ];
setTagValues( ( prev ) => ( {
...prev,
applyTags: settings.apply_tags || prev.applyTags,
applyDeletedTags:
settings.apply_tags_deleted || prev.applyDeletedTags,
applyCheckInTags:
settings.apply_tags_checkin || prev.applyCheckInTags,
addAttendees: settings.add_attendees || prev.addAttendees,
} ) );
}, [ ticketData, ticketId ] );
// eslint-disable-next-line react-hooks/exhaustive-deps
const saveTags = useCallback(
async ( newTagValues ) => {
const snakeCaseValues = {
apply_tags: newTagValues.applyTags,
apply_tags_deleted: newTagValues.applyDeletedTags,
apply_tags_checkin: newTagValues.applyCheckInTags,
add_attendees: newTagValues.addAttendees,
};
const dataString = JSON.stringify( snakeCaseValues );
if (
saveRequestInProgress.current ||
dataString === lastSavedData.current
) {
return;
}
// Skip if no actual data to send
if (
ticketData &&
JSON.stringify( ticketData[ ticketId ] ) === dataString
) {
return;
}
// Set flag to prevent concurrent requests
saveRequestInProgress.current = true;
try {
await apiFetch( {
path: '/wp-fusion/v1/tribe-tickets/update-ticket-tags',
method: 'POST',
data: {
nonce: window.wpfTribeTickets.nonce,
ticketId,
...newTagValues,
},
} );
lastSavedData.current = dataString;
} catch ( err ) {
setError( 'Failed to update tags: ' + err );
} finally {
setTimeout( () => {
saveRequestInProgress.current = false;
}, 2000 );
}
},
[ ticketData, ticketId ]
);
// Add the saveTagSettings function to the window object to be executed when the save button is clicked.
useEffect( () => {
if ( window.wpfTribeTickets ) {
window.wpfTribeTickets.saveTicketsSettings = () => {
if ( ! isDisabled ) {
saveTags( tagValues );
}
};
}
return () => {
if ( window.wpfTribeTickets ) {
delete window.wpfTribeTickets.saveTicketsSettings;
}
};
}, [ tagValues, isDisabled, saveTags ] );
return (
<InspectorControls>
<PanelBody title={ __( 'WP Fusion', 'wp-fusion' ) } initialOpen>
<SettingsPanel
tagValues={ tagValues }
setTagValues={ setTagValues }
isDisabled={ isDisabled }
/>
{ isDisabled && (
<p
className="components-panel__body-content"
style={ { color: 'red' } }
>
{ error
? error
: __(
'You should first save the ticket before setting the WP Fusion settings.',
'wp-fusion'
) }
</p>
) }
</PanelBody>
</InspectorControls>
);
};
#How did it get this way?
It’s our opinion that the block editor was rushed out too fast, without proper documentation, tooling, or guidelines for developers.
Early Gutenberg documentation was spread across multiple sources—GitHub issues, the Gutenberg plugin repo, Make WordPress blog posts, and partial entries in the WordPress Developer Handbook. There wasn’t a single canonical resource.
There were no clear architectural guidelines for folder structure, how to handle block styles, or how to use components from @wordpress/components
or @wordpress/editor
.
Developers were left to reverse-engineer the Gutenberg source code or copy from the examples in the @wordpress
GitHub monorepo.
Tooling was virtually non-existent at first. There was no official scaffolding tool to set up new blocks, and for the first two years, developers relied on create-guten-block by Ahmad Awais or Advanced Custom Fields to create blocks, before WordPress eventually released the official Create Block scaffolding tool in 2020.
Meanwhile, developers were pushed hard to adopt the new editor. Block-based themes and plugins were given prominence in the WordPress.org directory, and block development topics featured heavily on the official blog and at WordCamps.
We were told, “This is the future, adapt or be left behind” without a clear definition of what that future looked like.
This is reflected in the timing of The Events Calendar and GiveWP block editor integrations.
TEC was early to the party, releasing a block editor integration for Event Tickets in 2018.
GiveWP’s “Visual Form Builder” wasn’t released until the end of 2023 when more documentation and standards were in place.
This is nothing against the team at The Events Calendar, I know they are talented developers, and they did their best with the resources available at the time.
Unfortunately, this early adoption has left TEC loaded with technical debt that will be expensive (if not impossible) to unburden, and made third-party integrations like WP Fusion’s exponentially more difficult to implement.
#Conclusion
As we look toward upcoming technologies from WordPress core and Automattic—like Full Site Editing, the admin redesign, and WooCommerce’s block-based checkout—we’re seeing the same story repeat itself: a shiny new feature is released, shipped into production, and developers are left scrambling to adapt with limited documentation, minimal tooling, and narrow, single-purpose APIs.
WordPress has become what it is because, historically, the codebase was dependable, well-documented, and extendable. That openness invited a community to grow around it—and the most successful plugins have embraced that same philosophy.
When a plugin is designed to be extendable, it becomes a platform. It allows others to integrate with your product, which leads to better features, more customers, and shared success. The rising tide lifts all our ships.
I encourage everyone working in WordPress—core contributors and plugin developers alike—to keep this in mind from the very beginning: How will others build on your work? Can a small agency or solo developer understand what you’ve created and make it work for their clients? If not, maybe it’s time to take a step back and write the docs you wish had existed when you started.