Get post ancestors in the Block Editor

August 20, 2022 Update: For completeness, I added a JavaScript/Gutenberg solution which does what the findParent() function in question tries to do, i.e. find the top ancestor. You can find the code below the “Original Answer” section.


Original Answer

The block editor doesn’t (currently) have a function equivalent to get_post_ancestors(), and yes you could create such function on your own, but an easy way to get the first/highest-level ancestor without having to manually make multiple or even any REST API (or HTTP) requests, is:

  1. Use a custom REST API field (and just for certain post types like page in your case)

    This way, you wouldn’t need to make any additional API/HTTP requests and for the current Page/post, simply read from the post data that has already been fetched by the block editor.

  2. Or use a custom REST API endpoint

Working Examples

  1. Using a custom REST field named first_parent, but just use any other name you like:

    add_action( 'rest_api_init', 'my_register_rest_field' );
    function my_register_rest_field() {
        register_rest_field( 'page', 'first_parent', array(
            'get_callback' => function ( $post_arr ) {
                $ancestors = get_post_ancestors( $post_arr['id'] );
                return (int) array_pop( $ancestors );
            },
        ) );
    }
    

    So with that, you could simply call the getCurrentPostAttribute() to get the very first ancestor:

    wp.data.select( 'core/editor' ).getCurrentPostAttribute( 'first_parent' );
    
  2. Using a custom REST API endpoint which returns the first ancestor of a post, just like the above first_parent field:

    add_action( 'rest_api_init', 'my_register_rest_route' );
    function my_register_rest_route() {
        register_rest_route( 'my-plugin/v1', '/first-parent/(?P<id>\d+)', array(
            'methods'             => 'GET',
            'callback'            => function ( WP_REST_Request $request ) {
                $ancestors = get_post_ancestors( $request['id'] );
                return (int) array_pop( $ancestors );
            },
            'permission_callback' => '__return_true',
        ) );
    }
    

    So with that, you just need to make a fetch request to my-plugin/v1/first-parent/<post id>, e.g. https://example.com/wp-json/my-plugin/v1/first-parent/123, and you’ll get the very first ancestor of the post.

    wp.apiFetch( {
        path: 'my-plugin/v1/first-parent/123',
    } ).then(
        parent => console.log( parent ),
        error => console.log( error )
    );
    

And if you want, your endpoint could return an array of ancestors instead and use JS to get the first ancestor (which is the last item, if you used get_post_ancestors()).


Update: A (custom) JavaScript/Gutenberg Solution

I decided to try with useSelect() and getEntityRecord(), and I thought you might want to try it out or maybe just check the code 🙂

  • Note: parentId is the ID of the parent of the current post (i.e. the post’s post_parent value), so if it’s not a 0, i.e. it’s not a top-level post, then we’ll find its first ancestor. As for the rest like how/where firstParentId should be used, see the sample implementation below.
const firstParentId = useSelect( select => {
    const { getEntityRecord } = select( 'core' );

    const findFirstParentId = ( id, type ) => {
        const post = getEntityRecord( 'postType', type, id );

        // If the post is a child post, i.e. it has a parent, fetch the parent
        // post. So we're making a recursive function call until the very first
        // parent is found.
        if ( post && undefined !== post.parent && post.parent > 0 ) {
            return findFirstParentId( post.parent, type );
        }

        // If we've found the top ancestor, return its ID.
        return post ? post.id : -1;
    };

    return parentId ? findFirstParentId( postId, postType ) : -1;
}, [ postId, postType, parentId ] );

Sample Implementation (tested working in WordPress v6.0.1)

So this is the entire code in my block’s edit() function which displays the very first ancestor’s ID and followed by a UL list of the ancestors including the current post:

const { postId, postType, parentId } = useSelect( select => {
    const {
        getCurrentPostId,
        getCurrentPostType,
        getCurrentPostAttribute,
    } = select( 'core/editor' );

    return {
        postId: getCurrentPostId(),
        postType: getCurrentPostType(),
        parentId: getCurrentPostAttribute( 'parent' ),
    };
}, [] );

const { firstParentId, theParents, isResolved } = useSelect( select => {
    const { getEntityRecord } = select( 'core' );

    const theParents = [];
    // this is true if the current post has no parent, or that we've found the
    // top ancestor
    let isResolved   = ( ! parentId );

    const findFirstParentId = ( id, type ) => {
        const post = getEntityRecord( 'postType', type, id );

        // If the request has been resolved and the post data is good, then
        // we add the post to the parent pages array. Each item contains the
        // post ID and title. I.e. array( <ID>, <Title> )
        post && theParents.push( [ post.id, post.title?.rendered ] );

        // If the post is a child post, i.e. it has a parent, fetch the parent
        // post. So we're making a recursive function call until the very first
        // parent is found.
        if ( post && undefined !== post.parent && post.parent > 0 ) {
            return findFirstParentId( post.parent, type );
        }

        isResolved = ( !! post );

        // If we've found the top ancestor, return its ID.
        return post ? post.id : -1;
    };

    return {
        firstParentId: parentId ? findFirstParentId( postId, postType ) : -1,
        theParents,
        // use this instead to display the top ancestor first
//      theParents: theParents.reverse(),
        isResolved,
    };
}, [ postId, postType, parentId ] );

return (
    <div { ...useBlockProps() }>
        <p>
            Current Post ID: { postId } (type: { postType })<br />
            Top Ancestor ID: {
                isResolved ? ( firstParentId >= 0 ? firstParentId : 'None' ) :
                'Loading..'
            }
        </p>

        <h3>Hierarchy</h3>
        { ! isResolved && ( <p>Loading..</p> ) }
        { isResolved && theParents.length ? (
            <ul>
                { theParents.map( ( [ id, title ] ) => (
                    <li key={ 'page-' + id }>{ title } (ID: { id })</li>
                ) ) }
            </ul>
        ) : <p>None?</p> }
    </div>
);