@wordpress/server-side-render
Version:
The component used with WordPress to server-side render a preview of dynamic blocks to display in the editor.
212 lines (193 loc) • 5.57 kB
text/typescript
/**
* WordPress dependencies
*/
import { debounce } from '@wordpress/compose';
import { useEffect, useState, useRef } from '@wordpress/element';
import apiFetch from '@wordpress/api-fetch';
import { addQueryArgs } from '@wordpress/url';
import { sanitizeBlockAttributes } from '@wordpress/blocks';
export function rendererPath(
block: string,
attributes: Record< string, unknown > | null = null,
urlQueryArgs: Record< string, unknown > = {}
): string {
return addQueryArgs( `/wp/v2/block-renderer/${ block }`, {
context: 'edit',
...( null !== attributes ? { attributes } : {} ),
...urlQueryArgs,
} );
}
export function removeBlockSupportAttributes(
attributes: Record< string, unknown > & {
style?: Record< string, unknown >;
}
): Record< string, unknown > {
const {
backgroundColor,
borderColor,
fontFamily,
fontSize,
gradient,
textColor,
className,
...restAttributes
} = attributes;
const {
border,
color,
elements,
shadow,
spacing,
typography,
...restStyles
} = attributes?.style || {};
return {
...restAttributes,
style: restStyles,
};
}
/**
* Server-side render response object.
*/
export interface ServerSideRenderResponse {
/** The current request status: 'idle', 'loading', 'success', or 'error'. */
status: 'idle' | 'loading' | 'success' | 'error';
/** The rendered block content (available when status is 'success'). */
content?: string;
/** The error message (available when status is 'error'). */
error?: string;
}
/**
* Configuration object for the useServerSideRender hook.
*/
export interface UseServerSideRenderArgs {
/** The block attributes to be sent to the server for rendering. */
attributes: Record< string, unknown >;
/** The identifier of the block to be serverside rendered. Example: 'core/archives'. */
block: string;
/** Whether to remove block support attributes before sending. */
skipBlockSupportAttributes?: boolean;
/** The HTTP method to use ('GET' or 'POST'). Default is 'GET'. */
httpMethod?: 'GET' | 'POST';
/** Additional query arguments to append to the request URL. */
urlQueryArgs?: Record< string, unknown >;
}
/**
* A hook for server-side rendering a preview of dynamic blocks to display in the editor.
*
* Handles fetching server-rendered previews for blocks, managing loading states,
* and automatically debouncing requests to prevent excessive API calls. It supports both
* GET and POST requests, with POST requests used for larger attribute payloads.
*
* @example
* Basic usage:
*
* ```jsx
* import { RawHTML } from '@wordpress/element';
* import { useServerSideRender } from '@wordpress/server-side-render';
*
* function MyServerSideRender( { attributes, block } ) {
* const { content, status, error } = useServerSideRender( {
* attributes,
* block,
* } );
*
* if ( status === 'loading' ) {
* return <div>Loading...</div>;
* }
*
* if ( status === 'error' ) {
* return <div>Error: { error }</div>;
* }
*
* return <RawHTML>{ content }</RawHTML>;
* }
* ```
*
* @param args The hook configuration object.
*
* @return The server-side render response object.
*/
export function useServerSideRender(
args: UseServerSideRenderArgs
): ServerSideRenderResponse {
const [ response, setResponse ] = useState< ServerSideRenderResponse >( {
status: 'idle',
} );
const shouldDebounceRef = useRef< boolean >( false );
const {
attributes,
block,
skipBlockSupportAttributes = false,
httpMethod = 'GET',
urlQueryArgs,
} = args;
let sanitizedAttributes: Record< string, unknown > | null =
attributes && sanitizeBlockAttributes( block, attributes );
if ( skipBlockSupportAttributes && sanitizedAttributes ) {
sanitizedAttributes =
removeBlockSupportAttributes( sanitizedAttributes );
}
// If httpMethod is 'POST', send the attributes in the request body instead of the URL.
// This allows sending a larger attributes object than in a GET request, where the attributes are in the URL.
const isPostRequest = 'POST' === httpMethod;
const urlAttributes = isPostRequest ? null : sanitizedAttributes;
const path = rendererPath( block, urlAttributes, urlQueryArgs );
const body = isPostRequest
? JSON.stringify( { attributes: sanitizedAttributes ?? null } )
: undefined;
useEffect( () => {
const controller = new AbortController();
const debouncedFetch = debounce(
function () {
{
setResponse( { status: 'loading' } );
apiFetch< { rendered: string } >( {
path,
method: isPostRequest ? 'POST' : 'GET',
body,
headers: isPostRequest
? {
'Content-Type': 'application/json',
}
: {},
signal: controller.signal,
} )
.then( ( res ) => {
setResponse( {
status: 'success',
content: res ? res.rendered : '',
} );
} )
.catch( ( error: unknown ) => {
// The request was aborted, do not update the response.
if (
error instanceof Error &&
error.name === 'AbortError'
) {
return;
}
setResponse( {
status: 'error',
error:
error instanceof Error
? error.message
: String( error ),
} );
} )
.finally( () => {
// Debounce requests after first fetch.
shouldDebounceRef.current = true;
} );
}
},
shouldDebounceRef.current ? 500 : 0
);
debouncedFetch();
return () => {
controller.abort();
debouncedFetch.cancel();
};
}, [ path, isPostRequest, body ] );
return response;
}