The WordPress data module provides React hooks that enable components to interact with stores. This guide covers the essential patterns for reading data with useSelect and modifying data with useDispatch.
Reading Data with useSelect
The useSelect hook subscribes to store data and re-renders your component when that data changes.
Basic Usage
import { useSelect } from '@wordpress/data';
import { store as coreDataStore } from '@wordpress/core-data';
function PagesList() {
const pages = useSelect( ( select ) => {
return select( coreDataStore ).getEntityRecords( 'postType', 'page' );
}, [] );
return (
<ul>
{ pages?.map( ( page ) => (
<li key={ page.id }>{ page.title.rendered }</li>
) ) }
</ul>
);
}
The useSelect hook takes two arguments:
- Callback function: Receives
select as first argument, returns derived data
- Dependencies array: Triggers recalculation when values change
With Query Parameters
Pass query parameters to filter and control API requests:
function SearchablePages() {
const [ searchTerm, setSearchTerm ] = useState( '' );
const pages = useSelect( ( select ) => {
const query = {};
if ( searchTerm ) {
query.search = searchTerm;
}
return select( coreDataStore ).getEntityRecords( 'postType', 'page', query );
}, [ searchTerm ] );
return (
<div>
<SearchControl onChange={ setSearchTerm } value={ searchTerm } />
<PagesList pages={ pages } />
</div>
);
}
Always include values used inside the callback in the dependencies array. This ensures the selector re-runs when those values change.
Multiple Selectors
Return an object to use multiple selectors:
const { pages, hasResolved } = useSelect( ( select ) => {
const query = {};
if ( searchTerm ) {
query.search = searchTerm;
}
const selectorArgs = [ 'postType', 'page', query ];
return {
pages: select( coreDataStore ).getEntityRecords( ...selectorArgs ),
hasResolved: select( coreDataStore ).hasFinishedResolution(
'getEntityRecords',
selectorArgs
),
};
}, [ searchTerm ] );
if ( ! hasResolved ) {
return <Spinner />;
}
Resolution Status
Track whether data is still loading using resolution selectors:
const { pages, hasResolved, isResolving } = useSelect( ( select ) => {
const selectorArgs = [ 'postType', 'page', query ];
return {
pages: select( coreDataStore ).getEntityRecords( ...selectorArgs ),
hasResolved: select( coreDataStore ).hasFinishedResolution(
'getEntityRecords',
selectorArgs
),
isResolving: select( coreDataStore ).isResolving(
'getEntityRecords',
selectorArgs
),
};
}, [ query ] );
hasStartedResolution: Returns true if resolution has been triggered
isResolving: Returns true if resolution is in progress
hasFinishedResolution: Returns true if resolution completed
Always pass the exact same arguments to hasFinishedResolution as you pass to the selector. Store them in a variable to avoid typos.
Getting Selectors Directly
For event callbacks where you don’t need reactivity, get the selectors function:
import { useSelect } from '@wordpress/data';
import { store as myCustomStore } from 'my-custom-store';
function Paste( { children } ) {
const { getSettings } = useSelect( myCustomStore );
function onPaste() {
// Get settings at the time of the event
const settings = getSettings();
}
return <div onPaste={ onPaste }>{ children }</div>;
}
Warning: Don’t use this pattern in render - your component won’t re-render on data changes.
Writing Data with useDispatch
The useDispatch hook provides access to action creators for modifying state.
Basic Usage
import { useDispatch } from '@wordpress/data';
import { store as coreDataStore } from '@wordpress/core-data';
function DeleteButton( { pageId } ) {
const { deleteEntityRecord } = useDispatch( coreDataStore );
const handleDelete = async () => {
await deleteEntityRecord( 'postType', 'page', pageId );
};
return <button onClick={ handleDelete }>Delete</button>;
}
With Dynamic Data
Combine useSelect and useDispatch for actions based on current state:
import { useCallback } from 'react';
import { useDispatch, useSelect } from '@wordpress/data';
import { store as myCustomStore } from 'my-custom-store';
function SaleButton( { children } ) {
const { stockNumber } = useSelect(
( select ) => select( myCustomStore ).getStockNumber(),
[]
);
const { startSale } = useDispatch( myCustomStore );
const onClick = useCallback( () => {
const discountPercent = stockNumber > 50 ? 10 : 20;
startSale( discountPercent );
}, [ stockNumber, startSale ] );
return <button onClick={ onClick }>{ children }</button>;
}
Using Registry Select in Dispatch
Access dynamic data at dispatch time using the registry:
import { useDispatch } from '@wordpress/data';
import { store as myCustomStore } from 'my-custom-store';
const SaleButton = withDispatch( ( dispatch, ownProps, { select } ) => {
const { getStockNumber } = select( myCustomStore );
const { startSale } = dispatch( myCustomStore );
return {
onClick() {
// Get fresh value at click time
const discountPercent = getStockNumber() > 50 ? 10 : 20;
startSale( discountPercent );
},
};
} )( Button );
Working with Entity Records
Fetching Entity Records
Use getEntityRecords for collections:
const pages = useSelect( ( select ) => {
return select( coreDataStore ).getEntityRecords( 'postType', 'page', {
per_page: 10,
status: 'publish',
} );
}, [] );
Use getEntityRecord for single records:
const page = useSelect( ( select ) => {
return select( coreDataStore ).getEntityRecord( 'postType', 'page', pageId );
}, [ pageId ] );
Entity Records vs Edited Entity Records
The data module distinguishes between:
- Entity Records: Data as fetched from the API (no local edits)
- Edited Entity Records: Data with local edits applied
// Returns persisted data only
select( 'core' ).getEntityRecord( 'postType', 'page', pageId ).title
// { "rendered": "My Page", "raw": "My Page" }
// Returns data with local edits applied
select( 'core' ).getEditedEntityRecord( 'postType', 'page', pageId ).title
// "My Updated Page" (string, not object)
Edited entity records store only the raw value as a string, not the rendered HTML. This is because JavaScript cannot properly render server-side dynamic content like shortcodes.
Editing Entity Records
import { useDispatch } from '@wordpress/data';
import { store as coreDataStore } from '@wordpress/core-data';
function EditForm( { pageId } ) {
const page = useSelect(
( select ) => select( coreDataStore ).getEditedEntityRecord(
'postType',
'page',
pageId
),
[ pageId ]
);
const { editEntityRecord } = useDispatch( coreDataStore );
const handleChange = ( title ) => {
editEntityRecord( 'postType', 'page', pageId, { title } );
};
return (
<TextControl
label="Page title:"
value={ page.title }
onChange={ handleChange }
/>
);
}
Saving Entity Records
function SaveButton( { pageId } ) {
const { saveEditedEntityRecord } = useDispatch( coreDataStore );
const { isSaving, hasEdits } = useSelect(
( select ) => ({
isSaving: select( coreDataStore ).isSavingEntityRecord(
'postType',
'page',
pageId
),
hasEdits: select( coreDataStore ).hasEditsForEntityRecord(
'postType',
'page',
pageId
),
}),
[ pageId ]
);
const handleSave = async () => {
const savedRecord = await saveEditedEntityRecord( 'postType', 'page', pageId );
if ( savedRecord ) {
// Success!
}
};
return (
<Button
onClick={ handleSave }
disabled={ ! hasEdits || isSaving }
>
{ isSaving ? 'Saving...' : 'Save' }
</Button>
);
}
Creating New Records
For new records without a pageId, use saveEntityRecord:
function CreateForm() {
const [ title, setTitle ] = useState( '' );
const { saveEntityRecord } = useDispatch( coreDataStore );
const { lastError, isSaving } = useSelect(
( select ) => ({
// Notice: no pageId argument
lastError: select( coreDataStore ).getLastEntitySaveError( 'postType', 'page' ),
isSaving: select( coreDataStore ).isSavingEntityRecord( 'postType', 'page' ),
}),
[]
);
const handleSave = async () => {
const savedRecord = await saveEntityRecord(
'postType',
'page',
{ title, status: 'publish' }
);
if ( savedRecord ) {
// Record created!
}
};
return (
<div>
<TextControl
label="Page title:"
value={ title }
onChange={ setTitle }
/>
<Button onClick={ handleSave } disabled={ isSaving }>
{ isSaving ? 'Creating...' : 'Create' }
</Button>
</div>
);
}
Error Handling
Save Errors
Check for errors after save operations:
const { lastError } = useSelect(
( select ) => ({
lastError: select( coreDataStore ).getLastEntitySaveError(
'postType',
'page',
pageId
),
}),
[ pageId ]
);
const handleSave = async () => {
const savedRecord = await saveEditedEntityRecord( 'postType', 'page', pageId );
if ( ! savedRecord ) {
// Save failed - lastError will contain details
}
};
// Display error to user
{ lastError && (
<div className="error">Error: { lastError.message }</div>
) }
Delete Errors
const { getLastEntityDeleteError } = useSelect( coreDataStore );
const { deleteEntityRecord } = useDispatch( coreDataStore );
const handleDelete = async () => {
const success = await deleteEntityRecord( 'postType', 'page', pageId );
if ( ! success ) {
const lastError = getLastEntityDeleteError( 'postType', 'page', pageId );
console.error( lastError.message );
}
};
Best Practices
Cache Benefits
The data module automatically caches responses:
// First call triggers API request
select( coreDataStore ).getEntityRecords( 'postType', 'page', { search: 'About' } );
// Subsequent calls use cached data
select( coreDataStore ).getEntityRecords( 'postType', 'page', { search: 'About' } );
This solves common problems like:
- Out-of-order API responses
- Duplicate requests
- Stale data
Consistent Selector Arguments
Ensure selector arguments match exactly:
const selectorArgs = [ 'postType', 'page', query ];
const { pages, hasResolved } = useSelect( ( select ) => ({
pages: select( coreDataStore ).getEntityRecords( ...selectorArgs ),
hasResolved: select( coreDataStore ).hasFinishedResolution(
'getEntityRecords',
selectorArgs
),
}), [ query ] );
Async Action Patterns
Actions return promises - use async/await:
const handleSave = async () => {
try {
const result = await saveEditedEntityRecord( 'postType', 'page', pageId );
if ( result ) {
console.log( 'Saved successfully' );
}
} catch ( error ) {
console.error( 'Save failed', error );
}
};
Next Steps