WordPress Gutenberg useEntityProp: Read and Write Post Meta from Block Edit Components

Gutenberg’s useEntityProp hook (from @wordpress/core-data) is the canonical way for block edit components to read and write post attributes — specifically post meta — that are stored in the WordPress database. Before useEntityProp, blocks used wp_localize_script() to pass meta values into the editor and useDispatch( 'core/editor' ).editPost() to write them back. useEntityProp replaces this two-step pattern with a single hook that returns a tuple of [value, setValue, fullObject], similar to React’s useState. When the value is updated via setValue, it is written to the entity store and the editor marks the post as having unsaved changes — persisting it to the REST API when the post is saved. The meta field must be registered with register_post_meta() with 'show_in_rest' => true for the hook to work.

Problem: A custom block needs to read and write a post meta field _featured_quote directly from the block editor sidebar, without a separate metabox. The value should update in real time when the user types, be part of the normal save flow, and be read server-side in a PHP render callback.

Solution: Register the meta with register_post_meta() exposing it to the REST API. Use useEntityProp('postType', postType, 'meta') in the block edit component to read and update the meta value.

<?php
// ── 1. Register the meta field with REST API exposure ─────────────────
add_action( 'init', function () {
    register_post_meta( 'post', '_featured_quote', [
        'type'          => 'string',
        'single'        => true,
        'default'       => '',
        'show_in_rest'  => true,            // REQUIRED for useEntityProp to work
        'auth_callback' => function () {
            return current_user_can( 'edit_posts' );
        },
        'sanitize_callback' => 'sanitize_textarea_field',
    ] );
} );

// ── 2. Render callback reads the meta server-side ─────────────────────
function render_featured_quote_block( array $attributes, string $content, WP_Block $block ): string {
    $post_id = $block->context['postId'] ?? get_the_ID();
    $quote   = get_post_meta( $post_id, '_featured_quote', true );

    if ( ! $quote ) return '';

    return sprintf(
        '<blockquote class="featured-quote"><p>%s</p></blockquote>',
        esc_html( $quote )
    );
}

// edit.js
import { useEntityProp } from '@wordpress/core-data';
import { useSelect }     from '@wordpress/data';
import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, TextareaControl } from '@wordpress/components';
import { __ } from '@wordpress/i18n';

export default function Edit() {
    const blockProps = useBlockProps();

    // Get the current post type (blocks can be used in multiple post types)
    const postType = useSelect(
        ( select ) => select( 'core/editor' ).getCurrentPostType(),
        []
    );

    // useEntityProp returns [ value, setValue, fullEntityObject ]
    // 'postType' = entity kind, postType = entity name, 'meta' = property key
    const [ meta, setMeta ] = useEntityProp( 'postType', postType, 'meta' );

    const quote    = meta?._featured_quote ?? '';
    const setQuote = ( val ) => setMeta( { ...meta, _featured_quote: val } );

    return (
        <>
            <InspectorControls>
                <PanelBody title={ __( 'Featured Quote', 'textdomain' ) }>
                    <TextareaControl
                        label={ __( 'Quote text', 'textdomain' ) }
                        value={ quote }
                        onChange={ setQuote }
                        help={ __( 'Displayed as a styled blockquote.', 'textdomain' ) }
                    />
                </PanelBody>
            </InspectorControls>

            <div { ...blockProps }>
                { quote
                    ? <blockquote className="featured-quote"><p>{ quote }</p></blockquote>
                    : <p className="placeholder">{ __( 'Enter a featured quote in the sidebar →', 'textdomain' ) }</p>
                }
            </div>
        </>
    );
}

NOTE: The meta key passed to useEntityProp via the meta property returns an object containing all registered meta fields for the post — access individual fields as meta._featured_quote. When calling setMeta(), always spread the full meta object and override only the key you want to change: setMeta({ ...meta, _featured_quote: val }) — overwriting the entire meta object with a single key would clear all other meta values. Meta fields with names starting with _ (underscore prefix) are "protected" in WordPress — they require a registered auth_callback in register_post_meta() and are hidden from the standard Custom Fields metabox. Without 'show_in_rest' => true in the register_post_meta() call, the field will not appear in the REST API response and useEntityProp will return undefined.