Using apiFetch for retrieving post data in Gutenberg

The question:

In a plugin I’m developing, I registered a custom REST API route to get some post data passing a post id:

function rest_api_init() {
    register_rest_route( 'custombase/v1', '/post/(?P<id>[d]+)', array(
        'methods'             => 'GET',
        'callback'            => 'get_post_rest',
        'permission_callback' => '__return_true',
        'args'                => array(
            'id' => array(
                'validate_callback' => function( $param, $request, $key ) {
                    return is_numeric( $param );
                }
            ),
        ),
    ) );
}

function get_post_rest( WP_REST_Request $request ) {
    $post = get_post( $request['id'] );
    if ( is_wp_error( $post ) ) {
        return $post;
    }

    $data = array(
        'ID'    => $post->ID,
        'title' => $post->post_title,
    );

    $response = new WP_REST_Response( $data, 200 );

    return $response;
}

I checked and the route is registered correctly as I can get info visiting, for example, the following URL:

https://example.com/wp-json/custombase/v1/post/1239

This is the result:

{"ID":1239,"title":"My example post title"}

As I understand, I need to use the apiFetch utility to make REST API requests to custom routes in Gutenberg. This is what I’m doing:

const mypost = apiFetch( { path: 'custombase/v1/post/1239' } ).then( ( post ) => {
    return post;
} );

But, instead of post object, I’m getting a Promise with the post object inside when I do a console.log( mypost ):

Promise { <state>: "pending" }
​<state>: "fulfilled"
​<value>: Array [ {…} ]
​​0: Object { ID: 1239, title: "My example post title" }
​​length: 1
​​<prototype>: Array []

How can I get the post object instead?

The Solutions:

Below are the methods you can try. The first solution is probably the best. Try others if the first one doesn’t work. Senior developers aren’t just copying/pasting – they read the methods carefully & apply them wisely to each case.

Method 1

I’m getting a Promise with the post object inside when I do a
console.log( mypost )

Yes, because apiFetch() indeed returns a Promise object, and apiFetch() doesn’t assign the response received from the server to the mypost constant, and the return value of your then() callback is also not going to be assigned to the mypost variable.

How can I get the post object instead?

As suggested by React, you would want to add mypost as a local state in your component, then make your XHR/AJAX request (using apiFetch(), the native window.fetch(), Axios or whatever) in the componentDidMount() method of a class component, or use the useEffect hook in a function component.

Example using apiFetch() with useEffect:

import { useState, useEffect } from '@wordpress/element';
import apiFetch from '@wordpress/api-fetch';

function MyComponent( { post_id } ) {
    const [ error, setError ]       = useState( null );
    const [ mypost, setPost ]       = useState( null );
    const [ isLoaded, setIsLoaded ] = useState( false );

    useEffect( () => {
        apiFetch( { path: `custombase/v1/post/${ post_id }` } ).then(
            ( result ) => {
                setIsLoaded( true );
                setPost( result );
            },
            ( error ) => {
                setIsLoaded( true );
                setError( error );
            }
        );
    }, [ post_id ] );

    if ( error ) {
        return <p>ERROR: { error.message }</p>;
    } else if ( ! isLoaded ) {
        return <p>Loading post { post_id }..</p>;
    } else if ( mypost && mypost.id ) {
        return <h3>Post <i>{ mypost.title || '#' + mypost.id }</i> loaded!</h3>;
    }
    return <p>No such post</p>;
}
// Sample usage: <MyComponent post_id="1239" />

Alternate Solution: Add an entity for your custom endpoint into the list of entities in the block editor.

  1. Add the entity using addEntities():

    import { dispatch } from '@wordpress/data';
    
    dispatch( 'core' ).addEntities( [{
        baseURL: '/custombase/v1/post',
        // The 'post' is not a post type - it's the "post" as in /post above. Also, "kind"
        // and "name" are not documented, so let's assume they form the above baseURL..
        kind: 'custombase/v1',
        name: 'post',
        label: 'Post or whatever',
    }] );
    
    // You can, for example, call the above (i.e. add the entity) before you register your
    // block type.
    
  2. Then use getEntityRecord() to fetch post data from the endpoint:

    const mypost = select( 'core' ).getEntityRecord( 'custombase/v1', 'post', 1239 );
    // Note that getEntityRecord() caches the results.
    
  3. And note that for getEntityRecord() to work correctly, your endpoint callback must use the lowercase id and not ID:

    // In the get_post_rest() function:
    
    $data = array(
        'id'    => $post->ID, // use id and *not* ID
        'title' => $post->post_title,
    );
    

So for example using useSelect, the above component (MyComponent) could now look like so:

import { useSelect } from '@wordpress/data';

function MyComponent( { post_id } ) {
    const { mypost, isLoading } = useSelect( ( select ) => {
        const args = [ 'custombase/v1', 'post', post_id ];

        return {
            mypost: select( 'core' ).getEntityRecord( ...args ),
            isLoading: select( 'core/data' ).isResolving( 'core', 'getEntityRecord', args )
        };
    }, [ post_id ] );

    if ( isLoading ) {
        return <p>Loading post { post_id }..</p>;
    } else if ( mypost && mypost.id ) {
        return <h3>Post <i>{ mypost.title || '#' + mypost.id }</i> loaded!</h3>;
    }
    return <p>No such post</p>;
}


All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

Leave a Comment