Hook
The WordPress Block Editor exposes a powerful data layer through Redux-like stores that let you read and modify editor state programmatically. Once you understand how to interact with this API, you can build blocks and plugins that feel genuinely integrated rather than bolted on. In this guide, we'll walk through coding with the Block Editor data API: a practical guide to reading and changing editor state reliably.
Understanding the Block Editor data layer
The Block Editor isn't just a visual interface—it's a state management system built on the WordPress data store registry. When you interact with blocks, the editor, or settings, that interaction flows through Redux-like reducers and selectors. Two hooks handle nearly all your direct interaction with this data: useSelect for reading and useDispatch for writing.
Think of the data layer as the single source of truth. Every block's attributes, the editor's selected state, undo history, and dirty status all live here. When you bypass the API and manipulate the DOM directly, you risk desynchronizing that state and breaking features like undo, collaboration, or validation.
useSelect to read data
useSelect is your primary tool for subscribing to store state. It takes a callback that receives a select function, and returns whatever you need to read. When that data changes, your component re-renders automatically.
import { useSelect } from '@wordpress/data';
function MyBlockInspector() {
const selectedBlockAttributes = useSelect( ( select ) => {
const { getSelectedBlock } = select( 'core/block-editor' );
const block = getSelectedBlock();
return block ? block.attributes : {};
}, [] );
return (
<div>
<p>Current alignment: { selectedBlockAttributes.align }</p>
</div>
);
}The dependency array (second argument) is critical. An empty array [] means the selector runs once on mount. If you need the selector to re-evaluate when props change, include those props. If you omit the array entirely, the selector runs on every render, which defeats memoization.
In a real scenario, imagine building a custom sidebar that displays metadata about all blocks in the current post. You'd use useSelect to fetch the block list and count specific block types:
const blockStats = useSelect( ( select ) => {
const { getBlocks } = select( 'core/block-editor' );
const blocks = getBlocks();
const imageCount = blocks.filter(
( b ) => b.name === 'core/image'
).length;
return { total: blocks.length, images: imageCount };
}, [] );This approach ensures your sidebar always reflects the true editor state without polling or manual event listeners.
useDispatch to change data
useDispatch returns action creators from a store. Unlike useSelect, it doesn't re-render your component when state changes—it's for dispatching side effects. Call the action creator whenever you need to modify state.
import { useDispatch } from '@wordpress/data';
function AlignmentSwitcher() {
const { updateBlockAttributes } = useDispatch( 'core/block-editor' );
const handleAlign = ( alignment ) => {
updateBlockAttributes(
select( 'core/block-editor' ).getSelectedBlockClientId(),
{ align: alignment }
);
};
return (
<button onClick={ () => handleAlign( 'center' ) }>
Center
</button>
);
}You'll notice we needed to call select inside the click handler to get the current selected block ID. It's tempting to cache this with useSelect, but that creates a different problem: you might dispatch an action for a stale block ID if the selection changed between renders.
A practical pattern for form-based UIs is collecting changes locally, then dispatching them all at once:
function AdvancedSettings() {
const [ localSettings, setLocalSettings ] = React.useState( {} );
const { updateBlockAttributes } = useDispatch( 'core/block-editor' );
const selectedBlockId = useSelect(
( select ) => select( 'core/block-editor' ).getSelectedBlockClientId(),
[]
);
const handleSave = () => {
updateBlockAttributes( selectedBlockId, localSettings );
setLocalSettings( {} );
};
return (
<div>
<input
onChange={ ( e ) => setLocalSettings( { ...localSettings, customField: e.target.value } ) }
/>
<button onClick={ handleSave }>Save</button>
</div>
);
}This pattern avoids spamming the store with tiny incremental updates as the user types, which keeps undo history cleaner and reduces re-renders.
Core stores: core, core/editor, core/block-editor
WordPress provides three main stores. Understanding which selectors live in which store prevents wasted debugging time.
core/block-editor is the most frequently used. It holds block-level state: the block list, selected block, block nesting, and inner block handling. Most of your block manipulation happens here.
core/editor wraps post-level metadata: the post title, excerpt, featured image, publish status, and document settings. Use this when you need to read or modify post-level data.
core is the generic WordPress store and contains user capabilities, site settings, and entity data (posts, taxonomies, media). It's typically read-only for most block development.
A realistic scenario: you're building a block that should only be insertable if the post has a featured image. You'd combine stores:
const canInsert = useSelect( ( select ) => {
const featuredImageId = select( 'core/editor' ).getEditedPostAttribute( 'featured_media' );
const userCan = select( 'core' ).canUser( 'create', 'posts' );
return Boolean( featuredImageId ) && userCan;
}, [] );The key lesson: core/block-editor for blocks, core/editor for posts, core for global data.
Subscribe carefully to avoid loops
Subscriptions to the data store are the most common source of infinite loops in Block Editor plugins. If a selector triggers a dispatch, which modifies the store, which re-runs the selector, you've created a loop.
Here's a dangerous pattern:
// DO NOT DO THIS
function BadComponent() {
const { updateBlockAttributes } = useDispatch( 'core/block-editor' );
useSelect( ( select ) => {
const block = select( 'core/block-editor' ).getSelectedBlock();
if ( block && !block.attributes.validated ) {
updateBlockAttributes( block.clientId, { validated: true } );
}
return block;
}, [] );
return <div>Block validation running</div>;
}Every time getSelectedBlock() returns, we dispatch. That dispatch modifies the store. The selector re-runs. Infinite loop.
The fix is to move the dispatch logic outside the selector, into a side effect that's only triggered when you actually want it:
// CORRECT
function GoodComponent() {
const { updateBlockAttributes } = useDispatch( 'core/block-editor' );
const block = useSelect(
( select ) => select( 'core/block-editor' ).getSelectedBlock(),
[]
);
React.useEffect( () => {
if ( block && !block.attributes.validated ) {
updateBlockAttributes( block.clientId, { validated: true } );
}
}, [ block?.clientId, updateBlockAttributes ] );
return <div>Block validation triggered once</div>;
}By moving the dispatch into useEffect, we explicitly control when the action fires. The dependency array ensures it only runs when the block ID actually changes, not on every render.
Another common trap: forgetting to memoize the selector function itself. If your selector creates objects inline, it triggers a re-render even when the underlying data hasn't changed:
// SLOW - creates new object every time
const data = useSelect( ( select ) => {
return {
block: select( 'core/block-editor' ).getSelectedBlock(),
post: select( 'core/editor' ).getCurrentPost(),
};
}, [] );
// BETTER - WordPress provides memoization via createSelector
import { createSelector } from '@wordpress/data';
const selectBlockAndPost = createSelector(
( select ) => [
select( 'core/block-editor' ).getSelectedBlock(),
select( 'core/editor' ).getCurrentPost(),
],
( [ block, post ] ) => ( { block, post } )
);Using createSelector ensures that if neither the block nor post changed, the returned object is identical, preventing downstream re-renders.
Common mistakes
- Forgetting the dependency array in useSelect. Without it, the selector runs on every render, breaking memoization and performance. Always include
[]for selectors that don't need re-evaluation, or list specific props. - Dispatching inside selectors. Selectors should be pure functions. Move side effects into
useEffecthooks or event handlers. - Caching block IDs across renders without re-evaluation. Block selection can change any time. Use
useSelectto stay synchronized, not a ref or local state. - Mixing stores without understanding scope. Don't try to get post data from
core/block-editoror block data fromcore/editor. Use the right store for the right data. - Not handling missing data gracefully. When a block is deselected, selectors return undefined. Always add null checks before accessing nested properties.
Key takeaways
useSelectreads from the store with automatic memoization and re-renders when data changes; always include a dependency array.useDispatchreturns action creators for modifying state; move dispatches out of selectors and into event handlers oruseEffect.- Use
core/block-editorfor blocks,core/editorfor post metadata, andcorefor global data. - Avoid infinite loops by separating data reading (selectors) from data writing (dispatches) into distinct lifecycle phases.
- Memoize complex selectors with
createSelectorto prevent unnecessary re-renders in consumer components.






Comments
Loading comments…