- Pre-requisites
- Big Ideas
- Data package
- Core-data package
This doc assumes you are familiar with Redux concepts such as actions, reducers, and selectors, as well as certain Gutenberg concepts such as Thunks. You may still be able to get the gist of the ideas here without these pre-requisites, but you are highly encouraged to get familiar with them first.
Gutenberg is part of WordPress core and frequently acts on the same data. There are posts, pages, taxonomies, widgets, navigation items and so on. The obvious way of using the data, would be to just request it from API whenever a given React component needs it. This would have a serious drawback though. Any other component wanting to use the same data, would have to request it as well. So now there is more than one request. Then, what if the data changes? Would all components re-request it? Would they even know to do so?
The data layer provides answers to all of these questions and more. It handles data synchronization for you, so you can focus on your component. For example, when you need to do something with a widget, you can access it with:
function MyComponent({ widgetId }) {
const widget = useSelect(
select => select( coreStore ).getWidget( widgetId )
);
// ...
}
Moreover, you can be sure that it’s the most recent version, and that no unnecessary HTTP requests were performed. But how does it all work? There are a few big concepts to discuss:
- Data package (
@wordpress/data
)- Selectors and resolvers
- React hooks
- Core-data package (
@wordpress/core-data
)- Entities
- Entity Records
- Data flow
- Reading Entity Records
- Editing Entity Records
- Saving Entity Records
- Deleting Entity Records
Selectors are simple functions that return a piece of data from the store. Resolvers are used to load the data when there is none available yet. Let’s see how they work in tandem in this minimal store:
const store = wp.data.createReduxStore( 'thermostat', {
// Essential functions
selectors: {
getTemperatureCelsius: ( state ) => state.temperature,
getTemperatureFarenheit: ( state ) => state.temperature * 1.8 + 32
},
resolvers: {
getTemperatureCelsius: () => ( { dispatch } ) => {
dispatch.receiveTemperature( 10 );
}
},
// Utility functions
__experimentalUseThunks: true,
actions: {
receiveTemperature: ( temperature ) => ({
type: 'RECEIVE_TEMPERATURE',
temperature
})
},
reducer( state={}, action ) {
const newState = {
...state
}
if ( action.type === 'RECEIVE_TEMPERATURE' ) {
newState.temperature = action.temperature;
} else if ( action.type === '@@INIT' ) {
newState.temperature = 0;
}
return newState;
}
} );
wp.data.register(store)
The @@INIT
action is dispatched when the store is instantiated, and so the initial state says temperature: 0
.
The getTemperatureCelsius
is a simple selector, it predictably returns 0
once the store was instantiated:
> wp.data.select('my-store').getTemperatureCelsius()
0
Note we didn’t provide the state
as an argument. The Gutenberg data layer handles that for us.
dependency for other selectors
import createSelector from 'rememo';
// This selector will only calculate the return value once,
// as long as `state.temperature` remains the same.
getTemperatureFarenheit: createSelector(
// The selector
( state ) => state.temperature * 1.8 + 32,
// The reference(s) upon which the computation depends:
( state ) => [ state.temperature ]
)
Read more about memoized selectors in rememo package documentation.
getTemperatureCelsius
is more special as there is a resolver registered under the same name. When getTemperatureCelsius
is called for the first time, it will receive the current state and return 0
, but the data layer will also call the related resolver. Since our resolver populates the state with the temperature, the second call will return the actual data:
> wp.data.select('thermostat').getTemperatureCelsius()
0
> wp.data.select('thermostat').getTemperatureCelsius()
10
Once the data is loaded, getTemperatureFarenheit
can do something with it:
> wp.data.select('thermostat').getTemperatureFarenheit()
50
As we’re about to learn, the resolvers may be asynchronous. How do you know when the data becomes available? The easiest way is to use the resolveSelect
utility instead of select
:
> wp.data
.resolveSelect('thermostat')
.getTemperatureCelsius()
.then(( temperature ) => console.log( temperature ))
10
resolveSelect
returns a promise that waits until the resolver finishes, runs the selector, then yields the final value.
Let’s zoom into our resolver:
getTemperatureCelsius: () => ( { dispatch } ) => {
dispatch.receiveTemperature( 10 );
}
It is a thunk that populates the state. Note that it does not return anything, nor are there any assumptions on how the data is loaded. The sole goal of this function is to populate the state, and it does so by dispatching the receiveTemperature
action when the data is ready.
In real world, data is often stored in APIs and needs to be loaded asynchronously. Fortunately, resolvers can be async too. Here’s a different way of loading the temperature:
getTemperatureCelsius: () => async ( { dispatch } ) => {
const response = await window.fetch( '/temperature' );
const result = await response.json();
dispatch.receiveCurrentTemperature( result.temperature );
}
An attentive reader may ask at this point Is this going to send a request every time I use the getTemperatureCelsius()
selector? Great question! The answer is no, thanks to the resolvers cache.
Resolvers are cached by the data layer. Subsequent calls to the same selectors will not trigger additional HTTP requests.
Let’s take a closer look at the thermostat
store. Once it is registered with wp.data.register(store)
, the actual state looks as follows:
{
metadata: {},
root: {
temperature: 0
}
}
The state managed by the developer lives in root
, and the state managed by the @wordpress/data
package lives in metadata
. Let’s take a closer look at the latter.
Firstly we call the getTemperatureCelsius
selector for the first time:
> wp.data.select('thermostat').getTemperatureCelsius()
null
First, getTemperatureCelsius
does not refer to the same function as we originally registered with the store (( state ) => state.temperature
). Instead, the data
package replaced it with a „resolved” version using the mapResolvers utility. The function we’re actually calling is selectorResolver
. It does two things:
- It runs the underlying selector and returns the result.
- It runs the underlying resolver, but only if it isn’t already running and wasn’t already fulfilled.
Note that the selector runs first, which means the resolver can’t affect its return value.
When the resolver runs for the first time, selectorResolver
acquires a lock through resolversCache.markAsRunning()
, and when it finishes, it releases it through resolversCache.clear()
. That’s how we’re sure the same resolver never runs multiple times in parallel.
As a store developer, you never need to worry about the resolversCache
API. It is internal, and resolves to resolve the unique timing challenges of the data
module. Outside of the data module you may lean on resolvers metadata.
The resolver call is surrounded by two special actions: START_RESOLUTION
and FINISH_RESOLUTION
. If we peeked at the dispatch history after getTemperatureCelsius()
is initially called, it would look like this:
{
type: 'START_RESOLUTION',
selectorName: 'getTemperatureCelsius',
args: []
}
{
type: 'RECEIVE_TEMPERATURE',
temperature: 10
}
{
type: 'FINISH_RESOLUTION',
selectorName: 'getTemperatureCelsius',
args: []
}
The second, third, and any other call would not call any additional dispatch
calls thanks to resolversCache()
.
This is how the Redux state looks like after the FINISH_RESOLUTION
:
{
metadata: {
getTemperatureCelsius: /*
A mapping with one entry:
[] => false
*/
},
root: {
temperature: 10
}
}
It means that the resolution of getTemperatureCelsius
with an empty arguments list ([]
) is not running at the time (false
).
As you may notice, the resolution is cached per arguments list. If we called the selector with a bogus argument:
> wp.data.select('thermostat').getTemperatureCelsius(2)
10
It would run the resolver again and create a new metadata entry like this:
{
metadata: {
getTemperatureCelsius: /*
A mapping with two entries:
[] => false
[2] => false
*/
},
root: {
temperature: 10
}
}
How is this useful? It allows you to check the resolution state of your data via metadata selectors.
The data
module adds a few special selectors to every store registered with resolvers:
hasStartedResolution(selectorName, args)
isResolving(selectorName, args)
hasFinishedResolution(selectorName, args)
The names say it all. Here’s an example:
> (register a new store)
> wp.data.select('thermostat').hasStartedResolution('getTemperatureCelsius', [])
false
> wp.data.select('thermostat').isResolving('getTemperatureCelsius’)
false
> wp.data.select('thermostat').getTemperatureCelsius()
0
> wp.data.select('thermostat').hasStartedResolution('getTemperatureCelsius', [])
true
> wp.data.select('thermostat').isResolving('getTemperatureCelsius’)
false
// Not resolving yet, the resolver called asynchronously after the selector runs
> setTimeout(() => {
console.log(wp.data.select('thermostat').isResolving('getTemperatureCelsius’))
});
true
There are also two low-level selectors used to reason about the low-level details of the metadata mapping. They are listed for completeness, but this document does not cover them in details:
getCachedResolvers()
getIsResolving(selectorName, args)
Let’s imagine the temperature reading changes every minute. We will simulate this behavior like:
getTemperatureCelsius: () => ( { dispatch } ) => {
const temperature = (new Date()).getMinutes();
dispatch.receiveCurrentTemperature( temperature );
}
The selector will only trigger the resolver the first time it runs, and then it will keep returning the same data. How can you get a fresh reading? You need to invalidate the resolver cache.
The data module adds a few special actions to every store with resolvers. We’ve already discussed startResolution
and finishResolution
, although indirectly. Now let’s talk about invalidateResolution( selectorName, args )
.
invalidateResolution
removes the specified entry from the metadata cache. Here’s how it works:
> wp.data.select('thermostat').getTemperatureCelsius()
0 // Initial value
> wp.data.select('thermostat').getTemperatureCelsius()
10 // Resolved reading value
// ... a few minutes pass ...
> wp.data.select('thermostat').getTemperatureCelsius()
10 // Redux state is still the same
> wp.data.dispatch('thermostat').invalidateResolution('getTemperatureCelsius', [])
Promise {<fulfilled>: {…}} // The resolution was invalidated
> wp.data.select('thermostat').getTemperatureCelsius()
10 // Remember, selector returns the current value before resolving
// The resolver runs again only now.
> wp.data.select('thermostat').getTemperatureCelsius()
15
As the name core-data
says, this package connects WordPress core and the data
package. To explain how it can be useful in everyday development, we need to discuss a few key concepts first.
An entity is a basic unit of information in core-data. Entities are conceptually similar to REST API resources, database entries, and class objects. A Post is an entity, so is a Taxonomy and a Widget. We will use the latter as our running example. Default entities are declared in entities.js
, and a minimal definition looks like this:
const defaultEntities = [
// ...
{
name: 'widget',
label: __( 'Widgets' ),
baseURL: '/wp/v2/widgets',
kind: 'root',
},
// ...
}
name: widget
is the Entity name, no surprises there.
label: __( 'Widgets' )
is a human-readable name. It may be in any user interface elements that have to refer to this Entity.
baseURL: '/wp/v2/widgets'
tells the data layer where to find an API Endpoint that can be used to interact with data of type widget
. This URL will be requested to retrieve records, perform searches, create new ones, as well as update and delete the existing records. Later on, you will see how the data layer retrieves the data through HTTP on your behalf.
kind: 'root'
is a namespace. It’s needed, because apart of the Entities that are statically declared in entities.js, there are also dynamically registered Entities. For example, certain custom post types may be exposed as Entities. If one of them was called widget
, it would overwrite the actual widget
entity. Kind exists to avoid these conflicts. loadPostTypeEntities()
registers custom post types in a conflict-free way by providing kind: 'postType'
.
While Entity refers to a data type, Entity Record refers to the actual data. A post with ID 15 would be an Entity Record, just like a specific text widget instance. Entity Records don’t have to define any specific fields, aside of the primary key, which is id
by default. GET requests to the baseURL
endpoint must return a list of Entity Records. For example, requesting /wp/v2/widgets
would return a response similar to:
[
{ id: "block", sidebar: "header", ...},
{ id: "block-2", sidebar: "header", ...},
]
But you said I don’t need to request the data, I may just select it instead. That’s correct! The data layer happily handles all the heavy lifting for you.
The Redux state used by core-data
resembles this one structure:
{
entities: {
data: {
root: {
widgets: {
queriedData: null
}
}
}
}
}
The core-data
store provides a named selector for each of the default entities. There is getTaxonomies()
, getWidgets()
, and so on. These selectors are not actually implemented from scratch: getWidgets()
is merely a shorthand for getEntityRecords( 'root', 'widget' )
.
getEntityRecords( kind, name, query ) returns the queried items if they exist in the store. The arguments are:
kind
– points to the correct Entityname
– points to the correct Entityquery
– an optional HTTP query that can help with things like filtering and pagination
As with any selector, the first call returns null
because the store is still empty:
> wp.data.select('core').getEntityRecords( 'root', 'widget' )
null
Only then the data is loaded by the related resolver.
The getEntityRecords
resolver requests the data from the API using the baseURL
defined in the Entity config earlier on. For widgets, it’s /wp/v2/widgets
. Once the request is finished, the resolver dispatches the receiveEntityRecords()
action to store the retrieved records:
{
type: 'RECEIVE_ITEMS',
items: [
{ id: "block", sidebar: "header", ...},
{ id: "block-2", sidebar: "header", ...},
],
query: {},
kind: 'root',
name: 'widget',
invalidateCache: false
}
The RECEIVE_ITEMS
reducer creates the new Redux state :
{
root: {
entities: {
records: {
root: {
widget: {
queriedData: {
items: {
default: {
"block-2": {
id: "block-2",
...
},
"block-3": {
id: "block-3",
...
},
{
// Similarly, add "block-4", "block-5", "block-6" following the same structure
}
}
},
itemIsComplete: {
default: {
"block-2": true,
"block-3": true,
"block-4": true,
"block-5": true,
"block-6": true
}
},
queries: {
default : {
"": {
itemIds: ["block-2", "block-3", "block-4", "block-5", "block-6"],
meta: {
totalItems: 5,
totalPages: 1
}
}
}
}
},
}
}
},
}
};
}
Let’s discuss each of the keys under entities.records.root.widget.queriedData
items: {
default: {
"block": { id: "block", sidebar: "header", ...},
"block-2": { id: "block-2", sidebar: "header", ...}
}
}
items
stores the records returned by the baseURL API Endpoint. The data is keyed by items IDs for faster lookups.
The default
wrapper denotes the context
query parameter. For example, calling getEntityRecords( 'root', 'widget', { context: 'edit' } )
would turn the items
state to:
items: {
default: {
"block": { id: "block", sidebar: "header", ...},
"block-2": { id: "block-2", sidebar: "header", ...}
},
edit: {
"block": { id: "block", sidebar: "header", ...},
"block-2": { id: "block-2", sidebar: "header", ...}
}
}
The distinction is useful, because the REST API may return different fields for different contexts.
itemIsComplete: {
default: {
"block": true,
"block-2": true
}
}
Stores the information about which records finished loading already. For now, it’s only used internally in core-data
as a dependency for the memoized selectors.
queries: {
default: {
"": ["block", "block-2]
}
}
Stores the ID of items the API returned in response to each query. The widgets endpoint does not support pagination, but if it did then we could call:
wp.data.select('core').getEntityRecords( 'root', 'widget', { per_page: 1 } )
And get a new state like
queries: {
default: {
"": ["block", "block-2"],
"per_page=1": ["block"]
}
}
The resolver also supports default query parameters. They may be configured via baseURLParams
entity configuration key:
const defaultEntities = [
// ...
{
kind: 'root',
name: 'widget',
label: __( 'Widgets' ),
baseURL: '/wp/v2/widgets',
baseURLParams: { per_page: 1 },
},
// ...
}
// ...
wp.data.select('core').getEntityRecords( 'root', 'widget' )
// Request sent to /wp/v2/widgets?per_page=1
Going back to getEntityRecords()
, we are now ready to move from the simplified definition to the actual one.
export function getEntityRecords( state, kind, name, query ) {
// Queried data state is prepopulated for all known entities. If this is not
// assigned for the given parameters, then it is known to not exist.
const queriedState =
state.entities.records?.[ kind ]?.[ name ]?.queriedData;
if ( ! queriedState ) {
return null;
}
return getQueriedItems( queriedState, query );
}
This selector uses getQueriedItems
to find the list of relevant IDs in queries
based on a query
, and then picks them from items
as a list.
As a result, the developer gets the following experience:
> wp.data.select('core').getEntityRecords( 'root', 'widget' )
null // ...resolvers running...
> wp.data.select('core').getEntityRecords( 'root', 'widget' )
[
{ id: "block", sidebar: "header", ...},
{ id: "block-2", sidebar: "header", ...},
]
Just like there core-data
provides a getWidgets()
shortcut, it also provides getWidget( key, query )
one. It is a shorthand for getEntityRecord( 'root', 'widget', key, query )
:
> wp.data.select('core').getEntityRecord( 'root', 'widget', 'block-2')
{id: 'block-2', ...}
Note that the record was returned immediately, without waiting for the resolver. This ie because getEntityRecord( kind, type, key, query )
sources the data from the same Redux state as getEntityRecords()
. If the Entity Record is already stored there, it can be used immediately without re-requesting. If it’s not, we still need to wait for the resolver:
> wp.data.select('core').getEntityRecord( 'root', 'widget', 'block-10')
null // ...resolution in progress...
> wp.data.select('core').getEntityRecord( 'root', 'widget', 'block-10')
{id: 'block-10', ...}
The query
arguments works in the same way as it does for getEntityRecords()
.
Some Entity Record fields contain Gutenberg blocks, one such field is post.content
. The API could simply return the raw block markup:
{
"id": 90,
"content": '<!-- wp:site-title /-->'
}
But this reveals a problem: not all blocks can be rendered by JavaScript, so this information wouldn’t be enough to provide used the visual preview. Returning only the rendered block wouldn’t suffice either – the raw markup is required for editing.
To resolve this pickle, the API returns both raw and rendered block markup for some fields:
{
"id": 90,
"content": {
raw: '<!-- wp:site-title /-->',
rendered: '<h1 class="wp-block-site-title"><a href="/" rel="home" >WordPress site</a></h1>'
}
}
Sometimes handling an object instead of a string is not handy, so getRawEntityRecord
collapses the content
object into the raw
string like this:
> wp.data.select('core').getEntityRecord('postType', 'post', 90)
{
"id": 90,
"content": {
raw: '<!-- wp:site-title /-->',
rendered: '<h1 class="wp-block-site-title"><a href="/" rel="home" >WordPress site</a></h1>'
}
}
> wp.data.select('core').getRawEntityRecord('postType', 'post', 90).content
{
"id": 90,
"content": '<!-- wp:site-title /-->'
}
How does it know which attributes to collapse? It reads the rawAttributes
property from the Entity config:
{
label: __( 'Post Type' ),
name: 'postType',
kind: 'root',
key: 'slug',
baseURL: '/wp/v2/types',
baseURLParams: { context: 'edit' },
rawAttributes: [ 'title', 'excerpt', 'content' ],
}
Suppose you want to update the title of a post. You can do that direcly in the post editor, but how could we change the title of a post programatically? How the block editor keep track of these changes?
They're tracked using the editEntityRecord
action:
wp.data.dispatch('core').editEntityRecord(
'postType',
'post',
'1',
{ title: 'My new post title' }
)
It dispatches the following action:
{
type: 'EDIT_ENTITY_RECORD',
kind: 'postType',
name: 'post',
recordId: '1',
edits: {
title: 'My new post title'
}
}
And updates the Redux state as follows:
{
"root": {
"entities": {
"records": {
"postType": {
"post": {
"edits": {
"1": {
"title": "My new post title"
}
}
}
}
}
}
}
}
Let’s unpack what just happened! different parts of that new state:
Stores the latest edits per record. If we were to call editEntityRecord
again,
wp.data.dispatch('core').editEntityRecord( 'postType', 'post', '1', { title: 'Another post title' })
the edits
branch would reflect the latest value.
{
"root": {
"entities": {
"records": {
"postType": {
"post": {
"edits": {
"1": {
"title": "Another post title"
}
}
}
}
}
}
}
}
To access the edits, use the getEntityRecordEdits()
selector:
export function getEntityRecordEdits( state, kind, name, recordId ) {
return get( state.entities.data, [ kind, name, 'edits', recordId ] );
}
Note that the queriedData
didn’t change. What happens if we call getEntityRecord
now?
> wp.data.select('core').getEntityRecord( 'postType', 'post', 1 )
{
id: 1,
...,
title: {raw: 'Hello world!', rendered: 'Hello world!'}
}
The title is still the original one Hello world!
. This is expected, as getEntityRecord
tells us about the most recent API data. To access the edited data, that only lives in the browser, we must use getEditedEntityRecord()
instead:
> wp.data.select('core').getEditedEntityRecord( 'postType', 'post', 1 )
{
id: 1,
...,
title: "Another post title"
}
getEditedEntityRecord
has a very simple implementation. It takes the output of getRawEntityRecord()
, and applies any edits on top of it:
const raw = getRawEntityRecord( state, kind, name, recordId );
const edited = getEntityRecordEdits( state, kind, name, recordId );
if ( ! raw && ! edited ) { return false; }
return {
...raw,
...edited,
};
The undo
& redo
functionality is managed by the @wordpress/undo-manager
package that exposes a createUndoManager()
to create undoManager
instances.
The undoManager
reducer initalizes root.undoManager
with an instance of undoManager
import { createUndoManager } from '@wordpress/undo-manager';
...
export function undoManager( state = createUndoManager() ) {
return state;
}
The undoManager
instance has the following API
"undoManager":{
// to add records to the internal HistoryRecord
"addRecord": () => {},
// to undo last change in the internal HistoryRecord
"undo": () => {},
// to redo last undone change
"redo": () => {},
// are there changes that can be undone in the HistoryRecord?
"hasUndo": () => {},
// are there undone changes that can be redone?
"hasRedo":() => {}
},
This undoManager
instance maintains an internal HistoryRecord that can only be accesed through the above APIs
Every change dispatched with editEntityRecord
action is tracked via the addRecord
method and added to the undoManager's internal history.
export const editEntityRecord = (...) => {
...
select.getUndoManager().addRecord(...)
...
}
When changes are tracked they can be undone by dispatch the undo
action
export const undo =
() =>
( { select, dispatch } ) => {
const undoRecord = select.getUndoManager().undo();
if ( ! undoRecord ) {
return;
}
dispatch( {
type: 'UNDO',
record: undoRecord,
} );
};
This action (defined using Thunks) uses internally select.getUndoManager().undo()
to access the undo
instance method and return an action with the data to undo
{
type: 'UNDO',
record: [
{
id: {
kind: 'postType',
name: 'post',
recordId: '1'
},
changes: {
title: {
from: 'Hello world!',
to: 'My new post title'
}
}
}
]
}
This action is captured by the reducer that internally triggers a EDIT_ENTITY_RECORD
to update the state with the change.
Note
The withMultiEntityRecordEdits
HOR (defined in core-data/src/reducer.js
), that adds the that undo/redo reducer functionality, is applied to the entity
reducer, which manages the state for all entity types (e.g., posts, users, terms).
Here’s a practical demonstration:
> wp.data.select('core').getEntityRecord('postType','post',1).title
{raw: 'Hello world!', rendered: 'Hello world!'}
> wp.data.dispatch('core').editEntityRecord( 'postType', 'post', '1', { title: 'My new post title' })
Promise {...}
> wp.data.select('core').getEntityRecord('postType','post',1).title
{raw: 'Hello world!', rendered: 'Hello world!'}
> wp.data.select('core').getEditedEntityRecord('postType','post',1).title
'My new post title'
> wp.data.dispatch('core').undo()
Promise {...}
> wp.data.select('core').getEditedEntityRecord('postType','post',1).title
'Hello world!'
> wp.data.dispatch('core').redo()
Promise {...}
> wp.data.select('core').getEditedEntityRecord('postType','post',1).title
'My new post title'
In terms of Redux state transitions, undo
is not a separate action but a variant of EDIT_ENTITY_RECORD
that says isUndo: true
:
{
type: 'EDIT_ENTITY_RECORD',
kind: 'root',
name: 'widget',
recordId: 'block-2',
edits: {
sidebar: 'wp_inactive_widgets'
},
meta: {
isUndo: true
}
}
Dispatching that action does two things:
- Updates the
offset
tooffset - 1
- Re-computes
data.root.widget.edits
usingundo
entries up to theoffset
Here’s how the Redux state evolves as we do undo
and redo
:
> const peek = () => ({
edits: state.entities.data.root.widget.edits['block-2'],
undoOffset: state.undo.offset,
undoLength: state.undo.length
})
> peek()
{ edits: { sidebar: 'footer' }, undoOffset: 0, undoLength: 2 }
> wp.data.dispatch('core').undo()
{ edits: { sidebar: 'header' }, undoOffset: -1, undoLength: 2 }
> wp.data.dispatch('core').redo()
{ edits: { sidebar: 'footer' }, undoOffset: 0, undoLength: 2 }
Note the undo stack did not change, we merely moved the offset. Why? Because removing the undone entries from Redux state would make redo
impossible. In fact, sometimes that’s the desired behavior.
As a developer, you might want to update Entity Records in a way that doesn’t interfere with the undo feature. This is what the undoIgnore
option is for. Let’s see how it works in practice:
> wp.data.select('core').getEntityRecord('postType','post',2).title
{raw: 'Hello World!', rendered: 'Hello World!'}
> wp.data.dispatch('core').editEntityRecord( 'postType', 'post', '2', { title: 'My new post title' }, { undoIgnore: true })
Promise {...}
> wp.data.select('core').getEntityRecord('postType','post',2).title
{raw: 'Hello World!', rendered: 'Hello World!'}
> wp.data.select('core').getEditedEntityRecord('postType','post',2).title
'My new post title'
> wp.data.dispatch('core').undo()
Promise {...}
> wp.data.select('core').getEditedEntityRecord('postType','post',2).title
'My new post title'
In this scenario, the undo stack remains untouched.
Keep in mind, that using undo()
and redo()
will work just as if this update never happened.
If you need direct access to information related to edits, the following selectors will help:
wp.data.select('core').hasUndo()
wp.data.select('core').hasRedo()
wp.data.select('core').getEditedEntityRecord()
wp.data.select('core').hasEditsForEntityRecord()
wp.data.select('core').getEntityRecordNonTransientEdits()
The names say it all. If you would like to learn even more about their inner working, check the core-data reference documentation.
Now that the user updated the title, it is time to save changes. The easiest way to do it, is by using the saveEditedEntityRecord()
action:
wp.data.dispatch('core').saveEditedEntityRecord( 'root', 'widget', 'block-2' )
It collects all the edits for the specified Entity Record, applies them to the last queried state, and calls the API to perform the actual save operation.
After saveEditedEntityRecord
the value persisted will be by getEntityRecord
> wp.data.select('core').getEntityRecord('postType','post',1).title
{raw: 'Hello world!', rendered: 'Hello world!'}
> wp.data.dispatch('core').editEntityRecord( 'postType', 'post', '1', { title: 'My new post title' })
Promise {...}
> wp.data.select('core').getEntityRecord('postType','post',1).title
{raw: 'Hello world!', rendered: 'Hello world!'}
> wp.data.select('core').getEditedEntityRecord('postType','post',1).title
'My new post title'
> wp.data.dispatch('core').saveEditedEntityRecord( 'postType', 'post', 1 )
Promise {<pending>}
> wp.data.select('core').getEntityRecord('postType','post',1).title
{raw: 'My new post title', rendered: 'My new post title'}
> wp.data.select('core').getEditedEntityRecord('postType','post',1).title
'My new post title'
The hasEditsForEntityRecord
inform us if there edited values for an entity record
> wp.data.select('core').getEntityRecord('postType','post',1).title
{raw: 'Hello world!', rendered: 'Hello world!'}
> wp.data.dispatch('core').editEntityRecord( 'postType', 'post', '1', { title: 'My new post title' })
Promise {...}
> wp.data.select('core').hasEditsForEntityRecord( 'postType', 'post', 1)
true
> wp.data.dispatch('core').saveEditedEntityRecord( 'postType', 'post', 1 )
Promise {...}
> wp.data.select('core').getEntityRecord('postType','post',1).title
{raw: 'My new post title', rendered: 'My new post title'}
> wp.data.select('core').hasEditsForEntityRecord( 'postType', 'post', 1)
false
Under the hood saveEditedEntityRecord()
calls saveEntityRecord()
with all the edits related to the specified record. The following two variants are equivalent:
> wp.data.dispatch('core').editEntityRecord( 'postType', 'post', '1', { title: 'My new post title' })
> wp.data.dispatch('core').saveEditedEntityRecord( 'postType', 'post', 1 )
// The above is the same as
> wp.data.dispatch('core').saveEntityRecord( 'postType', 'post', { id: 1, title: 'My new post title' } )
saveEntityRecord
requests the API to update the existing record
or create a new one if now ID is provided
For example, saving a post without an ID will trigger a POST request to /wp/v2/posts
creating a new post:
> > wp.data.dispatch('core').saveEntityRecord( 'postType', 'post', { title: 'Bohemian Rhapsody' } ).then(console.log)
Promise {...}
{
...
id: 16,
title: {raw: 'Bohemian Rhapsody', rendered: 'Bohemian Rhapsody'}
...
}
While passing 1
as an ID will trigger a POST request to /wp/v2/posts/1
updatinf the referenced post:
> wp.data.dispatch('core').saveEntityRecord( 'postType', 'post', { id: 1, title: 'Radio Ga Ga' } ).then(console.log)
>
{
...
id: 1,
title: {raw: 'Radio Ga Ga', rendered: 'Radio Ga Ga'}
...
}
Once the record is saved and the API response with a new version is available, saveEntityRecord
dispatches the receiveEntityRecord
action with the new record. This way, the queriedItems
part of the Redux state is updated, and selecting the same record again reflects the saved changes:
> wp.data.select('core').getEntityRecord('postType','post',1).title
{raw: 'Radio Ga Ga', rendered: 'Radio Ga Ga'}
In Redux terms, saveEntityRecord
dispatches the following Redux actions:
{
type: 'SAVE_ENTITY_RECORD_START',
kind: 'postType',
name: 'post',
recordId: 1,
isAutosave: false
}
// ----------------------------------------
{
type: 'RECEIVE_ITEMS',
items: [
{
id: 1,
title: { raw: 'Innuendo', rendered: 'Innuendo' },
content: {
raw: '<!-- wp:paragraph -->\n<p>Welcome to WordPress. This is your first post. Edit or delete it, then start writing!</p>\n<!-- /wp:paragraph -->',
rendered: '\n<p>Welcome to WordPress. This is your first post. Edit or delete it, then start writing!</p>\n',
'protected': false,
block_version: 1
},
...
}
],
persistedEdits: {
id: 1,
title: 'Innuendo'
},
meta: undefined,
kind: 'postType',
name: 'post',
invalidateCache: true
}
// ----------------------------------------
{
type: 'SAVE_ENTITY_RECORD_FINISH',
kind: 'postType',
name: 'post',
recordId: 1,
error: undefined,
isAutosave: false
}
After SAVE_ENTITY_RECORD_START
, root.entities.records.postType.post.saving.1.pending
is set to true
And after SAVE_ENTITY_RECORD_FINISH
, root.entities.records.postType.post.saving.1.pending
is set to false
.
You may check if a record is currently being saved by calling isSavingEntityRecord
:
> wp.data.select('core').isSavingEntityRecord( 'postType','post',1 )
false
It returns the pending
value from the redux state.
If the API request fails, the final SAVE_ENTITY_RECORD_FINISH
will have an error
property like below:
{
type: 'SAVE_ENTITY_RECORD_FINISH',
kind: 'postType',
name: 'post',
recordId: 23,
error: {
code: 'rest_post_invalid_id',
message: 'Invalid post ID.',
data: {
status: 404
}
},
isAutosave: false
}
This property is stored in the redux state alongside pending
and may be accessed via the getLastEntitySaveError
selector.
Imagine we try to update a post with a non-existing ID:
> wp.data.dispatch('core').saveEntityRecord( 'postType', 'post', { id: 23, title: 'Village' } ).then(console.log)
Promise {...}
> wp.data.select('core').getLastEntitySaveError( 'postType', 'post', 23 )
{code: 'rest_post_invalid_id', message: 'Invalid post ID.', data: {...}}
The deletion logic is analogous to the saving logic, and there even are corresponding actions and selectors:
saveEntityRecord()
->deleteEntityRecord()
isSavingEntityRecord()
->isDeletingEntityRecord()
getLastEntitySaveError()
->getLastEntityDeleteError()
This page's content come from a hidden gem doc page about the Gutenberg data layer belonging to an open PR #37615. The document is very technical and explains in detail a lot of interesting things about WordPress Data layer that are not covered in the docs
I'm currently in the process of reviewing and updating the content. What I've updated so far over the original content:
I'm not opening a PR yet because I think this content could be part of a reshaped Data Layer Reference:
getBlockTypes
has the exact same documentation at:getBlockTypes
getBlockTypes
getEntityRecord
has same documentation at:getEntityRecord
getEntityRecord