Dealing with state in Gutenberg blocks, especially when you need blocks to talk to each other, can feel a bit like herding cats. But fear not! The @wordpress/data store is your secret weapon for sharing state across multiple Gutenberg blocks. In a nutshell, you create a central data store, and then your blocks can read from and write to that store, keeping everything in sync without endless prop drilling or complex callbacks. It’s like having a shared whiteboard where all your blocks can see and update information.
Gutenberg blocks are designed to be independent, which is great for modularity, but not so much when your blocks need to be aware of each other.
The Challenge of Isolated Blocks
Imagine you have an “Accordion” block and inside it, “Accordion Item” blocks. Each item needs to know if it’s currently open or closed, and the parent Accordion block might need to know which item is active to manage the overall layout or accessibility. Without shared state, you’d be passing information up and down a component tree, which quickly gets messy.
Beyond Prop Drilling
Another common scenario is a “Product Gallery” block and a “Product Image Viewer” block. When a user clicks an image in the gallery, you want the viewer to update. If these are separate top-level blocks or significantly distanced in the block hierarchy, traditional React prop passing becomes cumbersome. You’d have to pass a function from the main editor component down through potentially many layers to the gallery, and then have the gallery call this function to update the viewer block. This is “prop drilling” at its finest, and it’s not fun to maintain.
Centralizing Data for Better Management
Shared state with @wordpress/data allows you to centralize this information. Instead of each block managing its own small piece of the puzzle and trying to communicate directly, they all look to one source of truth. This makes your code cleaner, easier to understand, and much more maintainable as your block editor experience grows.
For those looking to deepen their understanding of state management in Gutenberg, a related article that provides valuable insights is available at this link. It offers a comprehensive overview of how to effectively utilize the @wordpress/data store to share state across multiple blocks, enhancing your ability to create dynamic and interactive WordPress experiences.
Core Concepts of @wordpress/data
Before diving into code, let’s quickly touch on the fundamental ideas behind @wordpress/data. Think of it as a simplified Redux, built specifically for WordPress.
Stores: The Heart of Your State
A store is like a dedicated container for a specific piece of your application’s state. You define what kind of data it holds and how that data can be changed. For instance, you could have a store for “product selections” or “accordion active items.” Each store has a unique namespace, which helps prevent conflicts.
Selectors: Reading Data
Selectors are functions that allow your blocks to read data from the store. They are pure functions, meaning they don’t modify the state; they just retrieve specific pieces of it. You might have a selector like getActiveAccordionItem() or getSelectedProductId().
Actions: Causing Changes
Actions are plain JavaScript objects that describe an event that has occurred. They are the only way to initiate a change in the store’s state. When you want to update something in the store, you “dispatch” an action. An action might look like { type: 'SET_ACTIVE_ITEM', itemId: 3 } or { type: 'UPDATE_PRODUCT_SELECTION', productId: 123 }.
Reducers: Processing Actions
Reducers are pure functions that take the current state and an action, and return a new state. They are responsible for making the actual changes to your data based on the actions dispatched. If a reducer doesn’t recognize an action type, it should return the current state unchanged.
Resolvers: Fetching Asynchronous Data (Optional but powerful)
Sometimes your initial state or subsequent state changes need to fetch data from an API (e.g., WordPress REST API). Resolvers handle these asynchronous operations. They dispatch actions once the data is fetched, which then update the state via reducers. While not always necessary for simple intra-block communication, they are incredibly powerful for more complex data needs.
Setting Up Your Custom Data Store
Let’s get practical. To share state, you first need to define your custom store. This usually happens in a dedicated file within your block’s src directory, perhaps src/store/index.js.
1. Defining Your Store Namespace
Your store needs a unique identifier. This is crucial to avoid clashes with other plugins or core WordPress stores. A good practice is to use your plugin’s domain or a very specific prefix.
“`javascript
// src/store/index.js
const STORE_NAMESPACE = ‘my-plugin/shared-accordion-state’;
export default STORE_NAMESPACE;
“`
2. Creating Actions
What kinds of changes do you want your blocks to be able to make? Let’s say for an accordion, blocks need to activate (open) and deactivate (close) an item, and perhaps toggle its state.
“`javascript
// src/store/actions.js
export const setActiveItem = ( itemId ) => {
return {
type: ‘SET_ACTIVE_ITEM’,
itemId,
};
};
export const clearActiveItem = () => {
return {
type: ‘CLEAR_ACTIVE_ITEM’,
};
};
export const toggleItem = ( itemId ) => {
return {
type: ‘TOGGLE_ITEM’,
itemId,
};
};
“`
Notice these are simple functions that return plain action objects. They only describe what happened, not how to change the state.
3. Writing Reducers
Now, how do we update the state based on those actions? This is where reducers come in. We’ll typically have an initial state, which is the state your store starts with.
“`javascript
// src/store/reducer.js
const DEFAULT_STATE = {
activeItemId: null, // No item active initially
};
const reducer = ( state = DEFAULT_STATE, action ) => {
switch ( action.type ) {
case ‘SET_ACTIVE_ITEM’:
return {
…state,
activeItemId: action.itemId,
};
case ‘CLEAR_ACTIVE_ITEM’:
return {
…state,
activeItemId: null,
};
case ‘TOGGLE_ITEM’:
return {
…state,
activeItemId: state.activeItemId === action.itemId ? null : action.itemId,
};
default:
return state; // Always return state if action is not recognized
}
};
export default reducer;
“`
It’s important that reducers are pure functions. They should not mutate the original state object directly. Instead, they return a new state object.
4. Defining Selectors
Selectors allow blocks to retrieve slices of your store’s state.
“`javascript
// src/store/selectors.js
export const getActiveItem = ( state ) => {
return state.activeItemId;
};
export const isActiveItem = ( state, itemId ) => {
return state.activeItemId === itemId;
}
“`
Again, selectors are pure functions. They simply read from the state.
5. Registering Your Store
Finally, bring all these pieces together and register your store with @wordpress/data. This is usually done in your main plugin file or in a central index.js file for your blocks.
“`javascript
// src/store/index.js (revisited)
import { createReduxStore, register } from ‘@wordpress/data’;
import reducer from ‘./reducer’;
import * as actions from ‘./actions’;
import * as selectors from ‘./selectors’;
const STORE_NAMESPACE = ‘my-plugin/shared-accordion-state’;
export const store = createReduxStore( STORE_NAMESPACE, {
reducer,
actions,
selectors,
// resolvers, // Add resolvers here if you have async data fetching
} );
register( store );
export default STORE_NAMESPACE; // Export the namespace for easy import
“`
Now your store is live and ready to be used by any block! Make sure this registration code runs when your blocks are initialized.
Consuming State in Your Editor Components
With your store set up, your blocks can now connect to it to read and write state. We’ll use the withSelect and withDispatch higher-order components (HOCs) from @wordpress/data (or the newer useSelect and useDispatch hooks for functional components).
Using withSelect to Read Data
withSelect injects data from the store as props into your component.
Example: An Icon/Button that shows the active Item ID
Let’s imagine a simple icon that displays the currently active item ID in your editor.
“`javascript
// src/blocks/accordion-parent/edit.js
import { withSelect } from ‘@wordpress/data’;
import STORE_NAMESPACE from ‘../../store’; // Adjust path as needed
// … (your block’s other imports) …
const MyIconComponent = ( { activeItemId } ) => {
return (
Active Item: { activeItemId || ‘None’ }
);
};
export default withSelect( ( select ) => {
// The select function gives you access to all registered stores.
const { getActiveItem } = select( STORE_NAMESPACE );
return {
activeItemId: getActiveItem(),
};
} )( MyIconComponent );
“`
Now, MyIconComponent will re-render automatically whenever activeItemId changes in your store.
Using withDispatch to Write Data
withDispatch injects functions that dispatch actions to your store as props.
Example: A Button to set the active Item ID
Let’s create a button that, when clicked, sets a specific item ID as active.
“`javascript
// src/blocks/accordion-item/edit.js
import { withDispatch } from ‘@wordpress/data’;
import STORE_NAMESPACE from ‘../../store’; // Adjust path as needed
const MyItemButton = ( { itemId, onActivateItem } ) => {
return (
Activate Item { itemId }
);
};
export default withDispatch( ( dispatch ) => {
// The dispatch function gives you access to dispatch actions for all registered stores.
const { setActiveItem } = dispatch( STORE_NAMESPACE );
return {
onActivateItem: ( itemId ) => {
setActiveItem( itemId );
},
};
} )( MyItemButton );
“`
When onActivateItem is called, it dispatches the SET_ACTIVE_ITEM action, which then updates the store. Any withSelect consumers will react to this change.
Combining withSelect and withDispatch
Often, a block needs to both read and write data. You can combine these HOCs.
“`javascript
// src/blocks/accordion-item/edit.js (more complete example)
import { compose } from ‘@wordpress/compose’;
import { withSelect, withDispatch } from ‘@wordpress/data’;
import STORE_NAMESPACE from ‘../../store’; // Adjust path as needed
const AccordionItemEditor = ( { attributes, setAttributes, isActive, onToggleItem } ) => {
const { itemId, title } = attributes;
return (
{ title }
This is content for item ID: { itemId }
{ isActive ? ‘Deactivate’ : ‘Activate’ }
{/ You might also have a RichText for the title /}
);
};
export default compose(
withSelect( ( select, ownProps ) => {
const { isActiveItem } = select( STORE_NAMESPACE );
const { itemId } = ownProps.attributes; // Use ownProps to get block attributes
return {
isActive: isActiveItem( itemId ),
};
} ),
withDispatch( ( dispatch, ownProps ) => {
const { toggleItem } = dispatch( STORE_NAMESPACE );
const { itemId } = ownProps.attributes;
return {
onToggleItem: () => {
toggleItem( itemId );
},
};
} )
)( AccordionItemEditor );
“`
This AccordionItemEditor block receives isActive from the store and can dispatch onToggleItem to change the store’s state.
If you’re looking to deepen your understanding of state management in WordPress, you might find it helpful to explore the article on how to effectively utilize the @wordpress/data store for sharing state across multiple Gutenberg blocks. This resource provides practical insights and examples that can enhance your development skills. For more information, you can check out this related article that discusses similar concepts and best practices in WordPress development.
Modern Approach: Hooks (useSelect, useDispatch)
If you’re using functional React components (which is the recommended approach for modern Gutenberg block development), hooks are generally preferred over HOCs due to their cleaner syntax and better readability.
useSelect for Reading Data
The useSelect hook takes a selector function and allows you to retrieve data from the store.
“`javascript
// src/blocks/accordion-parent/edit.js (with hooks)
import { useSelect } from ‘@wordpress/data’;
import STORE_NAMESPACE from ‘../../store’;
const MyIconComponent = () => {
const activeItemId = useSelect( ( select ) => {
const { getActiveItem } = select( STORE_NAMESPACE );
return getActiveItem();
}, [] ); // Empty dependency array means this selector runs once after initial render
return (
Active Item (Hook): { activeItemId || ‘None’ }
);
};
export default MyIconComponent;
“`
useSelect will automatically re-render the component if the selected data changes. The second argument (dependency array) is optional, but useful for optimization if your selector depends on props or state and needs to re-run only when those dependencies change. For simple store reads, an empty array or omitting it is often fine.
useDispatch for Writing Data
The useDispatch hook provides a way to get your store’s dispatchers.
“`javascript
// src/blocks/accordion-item/edit.js (with hooks)
import { useDispatch } from ‘@wordpress/data’;
import STORE_NAMESPACE from ‘../../store’;
const MyItemButton = ( { itemId } ) => {
const { setActiveItem } = useDispatch( STORE_NAMESPACE );
return (
Activate Item { itemId } (Hook)
);
};
export default MyItemButton;
“`
Now setActiveItem is directly available within your functional component.
Combining useSelect and useDispatch (Most Common Scenario)
“`javascript
// src/blocks/accordion-item/edit.js (hooks, more complete example)
import { useSelect, useDispatch } from ‘@wordpress/data’;
import STORE_NAMESPACE from ‘../../store’;
const AccordionItemEditor = ( { attributes, setAttributes } ) => {
const { itemId, title } = attributes;
const isActive = useSelect( ( select ) => {
const { isActiveItem } = select( STORE_NAMESPACE );
return isActiveItem( itemId );
}, [ itemId ] ); // Re-run selector if itemId changes
const { toggleItem } = useDispatch( STORE_NAMESPACE );
const onToggleItem = () => {
toggleItem( itemId );
};
return (
{ title }
This is content for item ID: { itemId }
{ isActive ? ‘Deactivate (Hook)’ : ‘Activate (Hook)’ }
{/ … other block controls … /}
);
};
export default AccordionItemEditor;
“`
This is a very common pattern for functional blocks that need to interact with a shared data store. It’s clean, efficient, and easy to follow.
Considerations and Best Practices
While @wordpress/data is powerful, keep these points in mind for a smooth development experience.
State Persistence (Editor vs. Frontend)
Crucial point: Data stored in @wordpress/data is for the editor experience only. When you save the post, the data in your @wordpress/data store is not automatically saved to the post content or post meta.
If you need the “active item” state to persist on the frontend (e.g., an accordion starts with a specific item open), you must:
- Save it to block attributes: Store a
defaultActiveItemattribute on your parent Accordion block. - Save it to post meta: If the state needs to be global to the post and not tied to a specific block instance, consider using
wp.data.core.editorto save post meta fields. - Frontend JavaScript: Replicate the state management logic on the frontend using plain JavaScript or a lightweight library if you need interactive shared state there.
Think of @wordpress/data as a scratchpad for the editor, allowing blocks to coordinate while the user is editing.
Granularity of Stores
Don’t throw everything into one giant store. If you have distinct sets of data that don’t directly relate (e.g., “product filters” and “form validation status”), create separate stores with their own namespaces. This improves organization and prevents unnecessary re-renders.
Performance Optimizations (Memorization)
Selectors can be computationally expensive if they do complex calculations or filter large arrays. @wordpress/data includes memoization out of the box (similar to reselect in Redux). This means if a selector is called with the same arguments, it returns the cached result without re-computing, as long as the underlying state hasn’t changed.
For useSelect, carefully consider the dependency array. Provide only the necessary dependencies to avoid unnecessary re-runs of your selector function.
Immutability is Key
Always remember that reducers must not mutate the original state object. Always return a new state object. If you’re modifying an array or object within the state, create a shallow copy first.
“`javascript
// BAD (mutates state directly)
state.items.push( newItem );
return state;
// GOOD (creates a new array)
return {
…state,
items: [ …state.items, newItem ],
};
“`
Violating immutability can lead to unpredictable behavior and hard-to-debug issues because @wordpress/data relies on shallow comparisons to detect state changes and trigger re-renders.
Error Handling
Consider how your store handles errors, especially if you introduce resolvers for async data. You might add isLoading and error properties to your state and dispatch corresponding actions to update them.
“`javascript
// Example of resolver logic
export function* fetchMyData() {
yield actions.setIsLoading( true );
try {
const response = yield apiFetch( { path: ‘/wp/v2/my-data’ } );
yield actions.receiveMyData( response );
} catch ( error ) {
yield actions.setError( error );
} finally {
yield actions.setIsLoading( false );
}
}
“`
Documentation
As your custom store grows, document its selectors, actions, and the shape of its state. This helps other developers (or your future self) understand how to interact with it.
By following these guidelines and leveraging the power of @wordpress/data, you can build complex, interactive Gutenberg blocks that communicate seamlessly, leading to a much more integrated and user-friendly editing experience.