0

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?

6
  • I'm not sure why you're using a custom endpoint to return post data, why not register the extra fields you wanted and use the get entity API? Then your posts would automatically be retrieved and would be incorporated in the standard post objects, and you could even use standard APIs to save the data. Doing it this way duplicates a lot of internal functionality, and introduces new problems, e.g. how do you tell the different between the current data vs the unsaved edited data?
    – Tom J Nowell
    May 28 at 13:09
  • as for the promise, this is an asynchronous mechanism, you need to wait for the promise to complete, but that's not a WordPress problem, that's a general javascript problem (and one you shouldn't be trying to solve as this approach is wrong, see my previous comment)
    – Tom J Nowell
    May 28 at 13:11
  • see developer.wordpress.org/reference/functions/register_rest_field/… for an example. If you're doing this because you believe you're improving performance by only retrieving the ID and title, then you are mistaken, the entire post will be fetched including its post meta and terms when get_post is called, and even if it was faster, it would still be slower than just bundling the data via the post endpoint and having it pre-added by the block editor so no additional requests are needed
    – Tom J Nowell
    May 28 at 13:13
  • Yes, I know how to use getEntityRecord to get post data and I'm sure it's way more optimal than using a custom endpoint, but it requires the post type to be specified. I just wanted to try using an agnostic endpoint that doesn't need the post type to be provided.
    – leemon
    May 28 at 13:29
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>;
}
4
  • Thanks! My knowledge of JS/React/Gutenberg is pretty limited and I'm learning as I go along. The async nature of JS is sometimes hard for me to grasp. I didn't know you could add custom entities to the list of entities in the block editor either.
    – leemon
    May 28 at 18:09
  • 1
    I could tell, which is also why I posted the answer than just linking to one my past answers.. And I was also once a JS newbie.. but you know, just keep on learning :) And here's a tip: Open the browser console and type wp.data.select or wp.data.dispatch or even just wp and hit the Enter key, and you'd see the list of methods/functions in the select, dispatch, etc. package. Even better, enable script debugging (SCRIPT_DEBUG) and it'll be easier for you to spot mistakes/errors in your code. ;)
    – Sally CJ
    May 28 at 18:39
  • 1
    BTW, I didn't notice this mistake in your original question: $data[], but getEntityRecord() is actually smart enough to return just one record.. :)
    – Sally CJ
    May 28 at 18:49
  • 1
    Yes, I spotted this mistake while testing some code. I edited my question.
    – leemon
    May 28 at 18:58

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.