A Gutenberg `publish_post` Equivalent

I’ve been porting bits and pieces (i.e., the main “meta box”) of Share on Mastodon to a “proper” Gutenberg “sidebar panel.”

One of the things the “classic” meta box does, is display the resulting Mastodon URL after1 a post is published (or updated).

In order to accomplish a similar thing in the block editor, I use wp.coreData.useEntityProp() to fetch the post’s custom fields, or rather, those fields that are made available to the editor.

const [ meta, setMeta ] = useEntityProp( 'postType', postType, 'meta' );

The thing is, all of the Mastodon API interaction—and that includes storing the status URL—happens on the server.

As a result, for new or previously “unshared” posts, the value of the front-end meta object’s _share_on_mastodon_url property will be … an empty string, even if the field has just been updated (in the background).

So, I need to somehow overwrite this with the new URL, once it is known, i.e., “some time” after publishing.

I need to listen to a “publish event” (and then refetch just the URL, probably from a custom API endpoint)!

data.subscribe()

Turns out most examples online2 still (?) use wp.data.subscribe() to check whether a post is done saving. Except, this approach is rather verbose, and possibly inefficient, as the “event” will fire multiple times. (Like, very many.)

It can be used, though, in combination with wp.element.useState(), to cook up something that fires only once.

const [ updated, setUpdated ] = wp.element.useState( false );

let wasSaving     = wp.data.select( 'core/editor' ).isSavingPost();
let wasAutosaving = wp.data.select( 'core/editor' ).isAutosavingPost();

wp.data.subscribe( () => {
   const isSaving     = wp.data.select( 'core/editor' ).isSavingPost();
   const isAutosaving = wp.data.select( 'core/editor' ).isAutosavingPost();
   const status       = wp.data.select( 'core/editor' ).getEditedPostAttribute( 'status' );

  const doneSaving  = wasSavingPost && ! wasAutosavingPost && ! isSavingPost && 'publish' === status;
  wasSavingPost     = isSavingPost;
  wasAutosavingPost = isAutosavingPost;

  if ( doneSaving ) { // This will be `true` quite a number of times.
    setUpdated( true );
  }
} );

if ( updated ) {
  // Code here runs just once.
}

Except, WordPress core doesn’t seem to be using such code anymore—unfortunately, I couldn’t exactly figure out what it does use. But it seems to be something like the following: a (much) more concise solution that relies on wp.data.useSelect(), which manages (some) state for us (and I keep reading you should use).

data.useSelect()

const doneSaving = wp.data.useSelect( ( select ) => {
  const isSaving     = select( 'core/editor' ).isSavingPost();
  const isAutosaving = select( 'core/editor' ).isAutosavingPost();
  const status       = select( 'core/editor' ).getEditedPostAttribute( 'status' );

  return isSaving && ! isAutosaving && 'publish' === status;
} );

if ( doneSaving ) {
  // Code here will run ... twice?
}

The example above, though, will run … twice. (When I tested it. Can’t 100% guarantee it won’t be more.) Probably good enough for my use case; almost certainly not okay if you want to, say, display a (single) notice.

Lastly, we can use a function, a combination of the previous solution and useState().

const doneSaving = () => {
  const { isSaving, isAutosaving, status } = wp.data.useSelect( ( select ) => {
    return {
      isSaving:     select( 'core/editor' ).isSavingPost(),
      isAutosaving: select( 'core/editor' ).isAutosavingPost(),
      status:       select( 'core/editor' ).getEditedPostAttribute( 'status' ),
    };
  } );

  const [ wasSaving, setWasSaving ] = wp.element.useState( isSaving && ! isAutosaving && 'publish' === status );

  if ( wasSaving ) {
    if ( ! isSaving ) {
      setWasSaving( false );
      return true;
    }
  } else if ( isSaving && ! isAutosaving && 'publish' === status ) {
    setWasSaving( true );
  }

  return false;
};

if ( doneSaving() ) {
  // Code here runs just once.
}

The nested if might look a bit clunky, but it works. And, it can be moved out of the main render function, for instance, or even out into a separate “helper object” (or library, or similar).

This last approach is the one I settled for, for now, in combination with the mentioned custom API endpoint and a short-ish delay of 1 second. Which might not be enough, if this whole thing is asynchronous and one needs to also post images and such to the Mastodon API. We’ll see, I guess.

Either way, it’s already improvement: before all this, the block editor would fall back to a “classic” meta box, which does not at all get reloaded like it does, together with the rest of the page, in the classic editor. And would thus never show the status URL (until the entire page is revisited at later time).

Or maybe I should eventually “pivot” to a notice, to avoid layout jumps from updating the sidebar panel’s HTML like that.

Also, if you do use “delayed sharing,” or the classic editor, none of this matters.

  1. (Almost) immediately after, if “delayed sharing” is disabled.
  2. Like, nearly all examples in this here GitHub issue.