The WordPress editors manipulate entity records - objects representing posts, pages, users, templates, and other WordPress data. The @wordpress/core-data package manages these records and provides a unified undo/redo system across multiple simultaneous edits.
What Are Entities?
An entity represents a data source. Each item within the entity is called an entity record. Entities are defined in the core-data package and map to WordPress REST API endpoints.
Entity Configuration
Entities are configured with the following properties:
kind
Groups related entities together (e.g., root, postType):
kind: 'root' // For core entities like users, taxonomies
kind: 'postType' // For post types like posts, pages
name
Unique identifier within the kind:
name: 'user'
name: 'page'
name: 'post'
baseURL
REST API endpoint path:
baseURL: '/wp/v2/pages'
baseURL: '/wp/v2/users'
Accessing Entities
Entities are accessed using kind and name:
// Get all pages
wp.data.select( 'core' ).getEntityRecords( 'postType', 'page' );
// Get a specific page
wp.data.select( 'core' ).getEntityRecord( 'postType', 'page', pageId );
// Get all users
wp.data.select( 'core' ).getEntityRecords( 'root', 'user' );
Dynamic Methods
For root kind entities, the package creates convenience methods:
// Instead of:
select( 'core' ).getEntityRecords( 'root', 'user' );
select( 'core' ).getEntityRecord( 'root', 'user', userId );
// You can use:
select( 'core' ).getUsers();
select( 'core' ).getUser( userId );
Entity Record States
The core-data store tracks two versions of each entity record:
Persisted Record
The last state fetched from the backend:
const page = select( 'core' ).getEntityRecord( 'postType', 'page', pageId );
// Returns: { title: { rendered: "My Page", raw: "My Page" }, ... }
This represents the saved state without any local modifications.
Edited Record
The persisted record with local edits applied:
const page = select( 'core' ).getEditedEntityRecord( 'postType', 'page', pageId );
// Returns: { title: "My Updated Page", ... }
Edited entity records contain raw values as strings, not objects with rendered and raw properties. This is because JavaScript cannot render server-side dynamic content.
Editing Entities
Basic Edit Flow
- Fetch the entity (automatically happens when you call a selector)
- Apply edits using
editEntityRecord
- Save changes using
saveEditedEntityRecord
// 1. Fetch (happens automatically in useSelect)
const page = useSelect(
( select ) => select( coreDataStore ).getEditedEntityRecord(
'postType',
'page',
pageId
),
[ pageId ]
);
// 2. Edit
const { editEntityRecord } = useDispatch( coreDataStore );
editEntityRecord( 'postType', 'page', pageId, { title: 'New Title' } );
// 3. Save
const { saveEditedEntityRecord } = useDispatch( coreDataStore );
await saveEditedEntityRecord( 'postType', 'page', pageId );
Edit Tracking
The store maintains a list of edits for each record:
// Check if there are unsaved edits
const hasEdits = select( 'core' ).hasEditsForEntityRecord(
'postType',
'page',
pageId
);
// Get the specific edits
const edits = select( 'core' ).getEntityRecordEdits(
'postType',
'page',
pageId
);
// Returns: { title: 'New Title' }
Complete Edit Example
import { useSelect, useDispatch } from '@wordpress/data';
import { store as coreDataStore } from '@wordpress/core-data';
import { TextControl, Button } from '@wordpress/components';
function EditPageForm( { pageId } ) {
const { page, hasEdits, isSaving } = useSelect(
( select ) => ({
page: select( coreDataStore ).getEditedEntityRecord(
'postType',
'page',
pageId
),
hasEdits: select( coreDataStore ).hasEditsForEntityRecord(
'postType',
'page',
pageId
),
isSaving: select( coreDataStore ).isSavingEntityRecord(
'postType',
'page',
pageId
),
}),
[ pageId ]
);
const { editEntityRecord, saveEditedEntityRecord } = useDispatch( coreDataStore );
const handleChange = ( title ) => {
editEntityRecord( 'postType', 'page', pageId, { title } );
};
const handleSave = async () => {
await saveEditedEntityRecord( 'postType', 'page', pageId );
};
return (
<div>
<TextControl
label="Page title:"
value={ page.title }
onChange={ handleChange }
/>
<Button
onClick={ handleSave }
disabled={ ! hasEdits || isSaving }
>
{ isSaving ? 'Saving...' : 'Save' }
</Button>
</div>
);
}
Creating New Records
For new records without an ID, use saveEntityRecord instead:
const { saveEntityRecord } = useDispatch( coreDataStore );
const handleCreate = async () => {
const newRecord = await saveEntityRecord(
'postType',
'page',
{
title: 'New Page',
status: 'publish',
content: 'Page content',
}
);
if ( newRecord ) {
console.log( 'Created page with ID:', newRecord.id );
}
};
Note the differences from editing:
- Use
saveEntityRecord (not saveEditedEntityRecord)
- Pass the complete record object (not just an ID)
- No
editEntityRecord call needed
Undo/Redo System
The WordPress editors support simultaneous editing of multiple entity records. The undo/redo system tracks all changes across entities.
How It Works
When editing in the Site Editor:
- Edit page title → Creates undo step
- Edit template content → Creates undo step
- Edit header template part → Creates undo step
- Press undo → Reverts the last change
Undo/Redo Stack Structure
Each modification stores:
- Entity kind and name: Identifies the entity (e.g.,
postType, page)
- Entity Record ID: The specific record modified
- Property: The modified property name (e.g.,
title)
- From: Previous value (for undo)
- To: New value (for redo)
Example stack:
[
{
kind: 'postType',
name: 'post',
id: 1,
property: 'title',
from: '',
to: 'Hello World'
},
{
kind: 'postType',
name: 'post',
id: 1,
property: 'slug',
from: 'previous-slug',
to: 'hello-world'
},
{
kind: 'postType',
name: 'wp_block',
id: 2,
property: 'title',
from: 'Reusable Block',
to: 'Awesome Reusable Block'
}
]
Using Undo/Redo
import { useDispatch, useSelect } from '@wordpress/data';
import { store as coreDataStore } from '@wordpress/core-data';
function UndoRedoButtons() {
const { hasUndo, hasRedo } = useSelect(
( select ) => ({
hasUndo: select( coreDataStore ).hasUndo(),
hasRedo: select( coreDataStore ).hasRedo(),
}),
[]
);
const { undo, redo } = useDispatch( coreDataStore );
return (
<div>
<Button onClick={ undo } disabled={ ! hasUndo }>
Undo
</Button>
<Button onClick={ redo } disabled={ ! hasRedo }>
Redo
</Button>
</div>
);
}
Cached Changes
Not all edits are immediately added to the undo/redo stack. Some modifications are “cached” to avoid creating excessive undo levels.
When Cached Changes Are Used
- Typing in text fields (creates undo level after a delay)
- Rapid successive changes
- Changes marked with
isCached: true option
// This edit is cached (not immediately added to undo stack)
editEntityRecord(
'postType',
'page',
pageId,
{ title: 'T' },
{ isCached: true }
);
// Cached changes are committed to undo stack when:
// 1. A non-cached edit occurs
// 2. __unstableCreateUndoLevel() is called
// 3. After a delay (in text input scenarios)
Transient Edits
Some edits don’t create undo levels at all:
editEntityRecord(
'postType',
'page',
pageId,
{ title: 'New Title' },
{ undoIgnore: true }
);
Transient edits are defined in entity configuration and typically include:
- UI state
- Temporary values
- Computed properties
Multiple Simultaneous Edits
The editor can edit multiple records at once:
// Edit page title
editEntityRecord( 'postType', 'page', 1, { title: 'New Page Title' } );
// Edit template content
editEntityRecord( 'postType', 'wp_template', 5, { content: '<!-- blocks -->' } );
// Edit template part
editEntityRecord( 'postType', 'wp_template_part', 3, { content: '<!-- header -->' } );
// All changes are tracked in the same undo/redo stack
When saving:
// Check if any entity has edits
const hasPagesEdits = select( 'core' ).hasEditsForEntityRecord( 'postType', 'page', 1 );
const hasTemplateEdits = select( 'core' ).hasEditsForEntityRecord( 'postType', 'wp_template', 5 );
// Save each edited entity
if ( hasPagesEdits ) {
await saveEditedEntityRecord( 'postType', 'page', 1 );
}
if ( hasTemplateEdits ) {
await saveEditedEntityRecord( 'postType', 'wp_template', 5 );
}
Entity Record Lifecycle
Best Practices
// ✅ Correct - reflects user edits
const page = select( 'core' ).getEditedEntityRecord( 'postType', 'page', pageId );
// ❌ Wrong - ignores user edits
const page = select( 'core' ).getEntityRecord( 'postType', 'page', pageId );
Check for Edits Before Saving
const hasEdits = select( 'core' ).hasEditsForEntityRecord(
'postType',
'page',
pageId
);
if ( hasEdits ) {
await saveEditedEntityRecord( 'postType', 'page', pageId );
}
Handle Save Errors
const savedRecord = await saveEditedEntityRecord( 'postType', 'page', pageId );
if ( ! savedRecord ) {
const error = select( 'core' ).getLastEntitySaveError(
'postType',
'page',
pageId
);
console.error( 'Save failed:', error.message );
}
Clear Edits When Needed
const { clearEntityRecordEdits } = useDispatch( coreDataStore );
// Discard all unsaved changes
clearEntityRecordEdits( 'postType', 'page', pageId );
Next Steps