diff --git a/packages/core-data/README.md b/packages/core-data/README.md
index 5740e5d6d5740..2a5631013e77a 100644
--- a/packages/core-data/README.md
+++ b/packages/core-data/README.md
@@ -764,6 +764,57 @@ In the above example, when `PageTitleDisplay` is rendered into an
application, the page and the resolution details will be retrieved from
the store state using `getEntityRecord()`, or resolved if missing.
+```js
+import { useState } from '@wordpress/data';
+import { useDispatch } from '@wordpress/data';
+import { __ } from '@wordpress/i18n';
+import { TextControl } from '@wordpress/components';
+import { store as noticeStore } from '@wordpress/notices';
+import { useEntityRecord } from '@wordpress/core-data';
+
+function PageRenameForm( { id } ) {
+ const page = useEntityRecord( 'postType', 'page', id );
+ const [ title, setTitle ] = useState( () => page.record.title.rendered );
+ const { createSuccessNotice, createErrorNotice } =
+ useDispatch( noticeStore );
+
+ if ( page.isResolving ) {
+ return 'Loading...';
+ }
+
+ async function onRename( event ) {
+ event.preventDefault();
+ page.edit( { title } );
+ try {
+ await page.save();
+ createSuccessNotice( __( 'Page renamed.' ), {
+ type: 'snackbar',
+ } );
+ } catch ( error ) {
+ createErrorNotice( error.message, { type: 'snackbar' } );
+ }
+ }
+
+ return (
+
+ );
+}
+
+// Rendered in the application:
+//
+```
+
+In the above example, updating and saving the page title is handled
+via the `edit()` and `save()` mutation helpers provided by
+`useEntityRecord()`;
+
_Parameters_
- _kind_ `string`: Kind of the entity, e.g. `root` or a `postType`. See rootEntitiesConfig in ../entities.ts for a list of available kinds.
diff --git a/packages/core-data/src/hooks/test/use-entity-record.js b/packages/core-data/src/hooks/test/use-entity-record.js
index fa67aded0c578..524c7d8348c08 100644
--- a/packages/core-data/src/hooks/test/use-entity-record.js
+++ b/packages/core-data/src/hooks/test/use-entity-record.js
@@ -50,7 +50,11 @@ describe( 'useEntityRecord', () => {
);
expect( data ).toEqual( {
- records: undefined,
+ edit: expect.any( Function ),
+ editedRecord: {},
+ hasEdits: false,
+ record: undefined,
+ save: expect.any( Function ),
hasResolved: false,
isResolving: false,
status: 'IDLE',
@@ -66,7 +70,11 @@ describe( 'useEntityRecord', () => {
} );
expect( data ).toEqual( {
+ edit: expect.any( Function ),
+ editedRecord: {},
+ hasEdits: false,
record: { hello: 'world', id: 1 },
+ save: expect.any( Function ),
hasResolved: true,
isResolving: false,
status: 'SUCCESS',
diff --git a/packages/core-data/src/hooks/use-entity-record.ts b/packages/core-data/src/hooks/use-entity-record.ts
index e751663f16804..a1f1a6c89fa07 100644
--- a/packages/core-data/src/hooks/use-entity-record.ts
+++ b/packages/core-data/src/hooks/use-entity-record.ts
@@ -1,7 +1,9 @@
/**
* WordPress dependencies
*/
+import { useDispatch, useSelect } from '@wordpress/data';
import deprecated from '@wordpress/deprecated';
+import { useMemo } from '@wordpress/element';
/**
* Internal dependencies
@@ -14,11 +16,25 @@ export interface EntityRecordResolution< RecordType > {
/** The requested entity record */
record: RecordType | null;
+ /** The edited entity record */
+ editedRecord: Partial< RecordType >;
+
+ /** Apply local (in-browser) edits to the edited entity record */
+ edit: ( diff: Partial< RecordType > ) => void;
+
+ /** Persist the edits to the server */
+ save: () => Promise< void >;
+
/**
* Is the record still being resolved?
*/
isResolving: boolean;
+ /**
+ * Does the record have any local edits?
+ */
+ hasEdits: boolean;
+
/**
* Is the record resolved by now?
*/
@@ -66,6 +82,58 @@ export interface Options {
* application, the page and the resolution details will be retrieved from
* the store state using `getEntityRecord()`, or resolved if missing.
*
+ * @example
+ * ```js
+ * import { useState } from '@wordpress/data';
+ * import { useDispatch } from '@wordpress/data';
+ * import { __ } from '@wordpress/i18n';
+ * import { TextControl } from '@wordpress/components';
+ * import { store as noticeStore } from '@wordpress/notices';
+ * import { useEntityRecord } from '@wordpress/core-data';
+ *
+ * function PageRenameForm( { id } ) {
+ * const page = useEntityRecord( 'postType', 'page', id );
+ * const [ title, setTitle ] = useState( () => page.record.title.rendered );
+ * const { createSuccessNotice, createErrorNotice } =
+ * useDispatch( noticeStore );
+ *
+ * if ( page.isResolving ) {
+ * return 'Loading...';
+ * }
+ *
+ * async function onRename( event ) {
+ * event.preventDefault();
+ * page.edit( { title } );
+ * try {
+ * await page.save();
+ * createSuccessNotice( __( 'Page renamed.' ), {
+ * type: 'snackbar',
+ * } );
+ * } catch ( error ) {
+ * createErrorNotice( error.message, { type: 'snackbar' } );
+ * }
+ * }
+ *
+ * return (
+ *
+ * );
+ * }
+ *
+ * // Rendered in the application:
+ * //
+ * ```
+ *
+ * In the above example, updating and saving the page title is handled
+ * via the `edit()` and `save()` mutation helpers provided by
+ * `useEntityRecord()`;
+ *
* @return Entity record data.
* @template RecordType
*/
@@ -75,7 +143,31 @@ export default function useEntityRecord< RecordType >(
recordId: string | number,
options: Options = { enabled: true }
): EntityRecordResolution< RecordType > {
- const { data: record, ...rest } = useQuerySelect(
+ const { editEntityRecord, saveEditedEntityRecord } =
+ useDispatch( coreStore );
+
+ const mutations = useMemo(
+ () => ( {
+ edit: ( record ) =>
+ editEntityRecord( kind, name, recordId, record ),
+ save: ( saveOptions: any = {} ) =>
+ saveEditedEntityRecord( kind, name, recordId, {
+ throwOnError: true,
+ ...saveOptions,
+ } ),
+ } ),
+ [ recordId ]
+ );
+
+ const { editedRecord, hasEdits } = useSelect(
+ ( select ) => ( {
+ editedRecord: select( coreStore ).getEditedEntityRecord(),
+ hasEdits: select( coreStore ).hasEditsForEntityRecord(),
+ } ),
+ [ kind, name, recordId ]
+ );
+
+ const { data: record, ...querySelectRest } = useQuerySelect(
( query ) => {
if ( ! options.enabled ) {
return null;
@@ -87,7 +179,10 @@ export default function useEntityRecord< RecordType >(
return {
record,
- ...rest,
+ editedRecord,
+ hasEdits,
+ ...querySelectRest,
+ ...mutations,
};
}