Blocks, Post meta and the Query Loop

When I was working on the CNHSA site, I couldn’t get MediaPress to wire up postmeta for whatever reason and I had already had the fields I wanted registered with ACF so I wrote a few hooks to get the data I needed to create a dyamic “address” block. Then, I tried using it inside a query loop block, and that failed.

After a little digging, here’s how to write a block that is compatible with the query loop.

The Data

The query loop block provides the following context:

"providesContext": {
  "queryId": "queryId",
  "query": "query",
  "displayLayout": "displayLayout",
  "enhancedPagination": "enhancedPagination"
},

Your block, then, just needs to consume that context!

In Practice

Here’s how I’m consuming that data in my custom useAcf hook.

// ./hooks/useAcf.tsx file

import { useState, useEffect } from '@wordpress/element';
import apiFetch from '@wordpress/api-fetch';
import { useSelect } from '@wordpress/data';
import { store as editorStore } from '@wordpress/editor';

type Context = {
	queryId?: number;
	postType?: string;
	postId?: number;
}

export default function useAcf( context: Context ) {
	const [ isLoading, setIsLoading ] = useState( true );
	const [ acf, setAcf ] = useState( [] );
	const isInQueryLoop = context?.queryId !== undefined;
	const [ postId, setPostId ] = useState<string | number | null>( context.postId || null );
	const [ postType, setPostType ] = useState<string | null>( context.postType || null );

	useSelect( ( select ) => {
		if ( ! isInQueryLoop ) {
			setPostId( select( editorStore ).getCurrentPostId() );
			setPostType( select( editorStore ).getCurrentPostType() );
		}
	}, [ isInQueryLoop ] );

	useEffect( () => {
		const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
		setIsLoading( true );
		apiFetch( { path: `/wp/v2/${ postType }/${ postId }`, signal: controller?.signal } )
			.then( ( postMeta ) => {
				setAcf( postMeta.acf );
			} ).catch( () => {
				setAcf( [] );
			} ).finally( () => {
				setIsLoading( false );
			} );
		return () => {
			controller?.abort();
			setIsLoading( false );
			setAcf( [] );
		};
	}, [ postType, postId ] );
	return { acf, isLoading };
}

What’s Happening:

At a high level, here’s what’s going on.

  1. Import the bits from WordPress
  2. Define the Context type for TS help
  3. init your variables with useState, setting them to default either to context’s args or null (if they don’t exist)
  4. use WordPress’s useSelect to hook to get the data from the editor if we aren’t in the query loop
  5. use a useEffect hook to fetch the ACF data from the Rest API (simple enough, but this assumes everything is set to show_in_rest
  6. Return the ACF data and the isLoading state to be handled in the component

See something inaccurate?