@@ -104,28 +118,30 @@ export default function ReusableBlockEdit( { attributes: { ref }, clientId } ) {
}
return (
-
-
-
- convertBlockToStatic( clientId ) }
- >
- { __( 'Convert to regular blocks' ) }
-
-
-
-
-
-
-
-
-
- {
}
+
+
+
+
+ convertBlockToStatic( clientId ) }
+ >
+ { __( 'Convert to regular blocks' ) }
+
+
+
+
+
+
+
+
+
-
+
);
}
diff --git a/packages/block-library/src/block/index.php b/packages/block-library/src/block/index.php
index 5fed48bd9a7c74..771adf27048db8 100644
--- a/packages/block-library/src/block/index.php
+++ b/packages/block-library/src/block/index.php
@@ -13,6 +13,8 @@
* @return string Rendered HTML of the referenced block.
*/
function render_block_core_block( $attributes ) {
+ static $seen_refs = array();
+
if ( empty( $attributes['ref'] ) ) {
return '';
}
@@ -22,6 +24,31 @@ function render_block_core_block( $attributes ) {
return '';
}
+ if ( in_array( $attributes['ref'], $seen_refs, true ) ) {
+ if ( ! is_admin() ) {
+ trigger_error(
+ sprintf(
+ // translators: %s is the user-provided title of the reusable block.
+ __( 'Could not render Reusable Block
%s: blocks cannot be rendered inside themselves.' ),
+ $reusable_block->post_title
+ ),
+ E_USER_WARNING
+ );
+ }
+
+ // WP_DEBUG_DISPLAY must only be honored when WP_DEBUG. This precedent
+ // is set in `wp_debug_mode()`.
+ $is_debug = defined( 'WP_DEBUG' ) && WP_DEBUG &&
+ defined( 'WP_DEBUG_DISPLAY' ) && WP_DEBUG_DISPLAY;
+
+ return $is_debug ?
+ // translators: Visible only in the front end, this warning takes the place of a faulty block.
+ __( '[block rendering halted]' ) :
+ '';
+ }
+
+ $seen_refs[] = $attributes['ref'];
+
if ( 'publish' !== $reusable_block->post_status || ! empty( $reusable_block->post_password ) ) {
return '';
}
diff --git a/packages/block-library/src/block/test/use-no-recursive-renders.js b/packages/block-library/src/block/test/use-no-recursive-renders.js
new file mode 100644
index 00000000000000..a9d9f6e53b789a
--- /dev/null
+++ b/packages/block-library/src/block/test/use-no-recursive-renders.js
@@ -0,0 +1,100 @@
+/**
+ * External dependencies
+ */
+import { render } from '@testing-library/react';
+
+/**
+ * WordPress dependencies
+ */
+import { Fragment } from '@wordpress/element';
+
+// Mimics a block's Edit component, such as ReusableBlockEdit, which is capable
+// of calling itself depending on its `ref` attribute.
+function Edit( { attributes: { ref } } ) {
+ const [ hasAlreadyRendered, RecursionProvider ] = useNoRecursiveRenders(
+ ref
+ );
+
+ if ( hasAlreadyRendered ) {
+ return
Halt
;
+ }
+
+ return (
+
+
+ { ref === 'SIMPLE' &&
Done
}
+ { ref === 'SINGLY-RECURSIVE' && (
+
+ ) }
+ { ref === 'MUTUALLY-RECURSIVE-1' && (
+
+ ) }
+ { ref === 'MUTUALLY-RECURSIVE-2' && (
+
+ ) }
+
+
+ );
+}
+
+/**
+ * Internal dependencies
+ */
+import useNoRecursiveRenders from '../use-no-recursive-renders';
+
+describe( 'useNoRecursiveRenders', () => {
+ it( 'allows a single block to render', () => {
+ const { container } = render(
+
+ );
+ expect(
+ container.querySelectorAll( '.wp-block__reusable-block' )
+ ).toHaveLength( 1 );
+ expect(
+ container.querySelectorAll( '.wp-block__reusable-block--halted' )
+ ).toHaveLength( 0 );
+ } );
+
+ it( 'allows equal but sibling blocks to render', () => {
+ const { container } = render(
+
+
+
+
+ );
+ expect(
+ container.querySelectorAll( '.wp-block__reusable-block' )
+ ).toHaveLength( 2 );
+ expect(
+ container.querySelectorAll( '.wp-block__reusable-block--halted' )
+ ).toHaveLength( 0 );
+ } );
+
+ it( 'prevents a block from rendering itself', () => {
+ const { container } = render(
+
+
+
+ );
+ expect(
+ container.querySelectorAll( '.wp-block__reusable-block' )
+ ).toHaveLength( 1 );
+ expect(
+ container.querySelectorAll( '.wp-block__reusable-block--halted' )
+ ).toHaveLength( 1 );
+ } );
+
+ it( 'prevents mutual recursion between two blocks', () => {
+ const { container } = render(
+
+
+
+ );
+ expect(
+ container.querySelectorAll( '.wp-block__reusable-block' )
+ ).toHaveLength( 2 );
+ expect(
+ container.querySelectorAll( '.wp-block__reusable-block--halted' )
+ ).toHaveLength( 1 );
+ } );
+} );
diff --git a/packages/block-library/src/block/use-no-recursive-renders.js b/packages/block-library/src/block/use-no-recursive-renders.js
new file mode 100644
index 00000000000000..b17ddd9b4f6186
--- /dev/null
+++ b/packages/block-library/src/block/use-no-recursive-renders.js
@@ -0,0 +1,29 @@
+/**
+ * WordPress dependencies
+ */
+import {
+ createContext,
+ useCallback,
+ useContext,
+ useMemo,
+} from '@wordpress/element';
+
+const RenderedRefsContext = createContext( [] );
+
+export default function useNoRecursiveRenders( ref ) {
+ const previouslyRenderedRefs = useContext( RenderedRefsContext );
+ const hasAlreadyRendered = previouslyRenderedRefs.includes( ref );
+ const newRenderedRefs = useMemo( () => [ ...previouslyRenderedRefs, ref ], [
+ ref,
+ previouslyRenderedRefs,
+ ] );
+ const Provider = useCallback(
+ ( { children } ) => (
+
+ { children }
+
+ ),
+ [ newRenderedRefs ]
+ );
+ return [ hasAlreadyRendered, Provider ];
+}