diff --git a/src/index.js b/src/index.js index e4d1b9a..3c0811b 100644 --- a/src/index.js +++ b/src/index.js @@ -639,20 +639,15 @@ addFilter( ); /** - * Placeholder shown in place of a Query Loop block until it scrolls into view. + * The core Query Loop immediately requests its post via REST on render, which + * can lock up the editor and hammer the server on a page with many query loops. * - * Rendering the real block edit component mounts the core Query Loop, which - * immediately fires a REST request to fetch its posts for the preview. On a - * page with many query loops every one of those requests is dispatched up - * front on editor load, which is slow and can hammer the server. This - * placeholder is cheap to render and issues no requests; the real block is - * only mounted once the placeholder intersects the viewport. + * This component renders a cheap placeholder that issues no requests while + * the Query Loop block is outside of the viewport, then initializes on demand. * - * The IntersectionObserver is created from the target node's own - * `defaultView` so the correct viewport is used whether or not the editor - * canvas is rendered inside an iframe (the site editor and the block-themed - * post editor both iframe the canvas). If IntersectionObserver is - * unavailable the block is rendered immediately as a safe fallback. + * Our IntersectionObserver is created from the target node's own `defaultView` + * so the correct viewport is used whether or not the editor is iframe'd. + * Blocks render as normal if IntersectionObserver is not available. * * @param {Object} props Component props. * @param {Function} props.onEnterViewport Called once the placeholder is at @@ -710,56 +705,70 @@ function QueryLoopPlaceholder( { onEnterViewport } ) { } /** - * Defer rendering of off-screen Query Loop blocks in the editor. + * Render a Query Loop block lazily, swapping in a placeholder until it is near + * the viewport. * - * Until a Query Loop scrolls near the viewport it is replaced with a cheap - * placeholder that fires no API requests. Selecting the block (e.g. right - * after inserting it, or via the List View) renders it immediately, and once - * a block has been rendered it stays rendered so scrolling away does not - * discard edits or trigger a refetch. + * Selecting the block (e.g. right after inserting it, or via the List View) + * renders it immediately, and once a block has been rendered it stays rendered + * so scrolling away does not discard edits or trigger a refetch. * - * This HOC is registered last so it wraps the plugin's other Query Loop - * enhancements: while the placeholder is shown none of them — nor the core - * block itself — are mounted. + * Kept as its own component — rather than inlined into the HOC below — so its + * hooks are never called after the HOC's early return for non-query blocks, + * which would violate the rules of hooks. + * + * @param {Object} props Block edit props. + * @param {Component} props.BlockEdit The wrapped block edit component. + * @return {Element} The rendered block or its placeholder. */ -const withViewportPlaceholder = createHigherOrderComponent( ( BlockEdit ) => { - return ( props ) => { - const { name, isSelected, clientId } = props; +function LazyBlockEdit( { BlockEdit, ...props } ) { + const { isSelected, clientId } = props; + + const [ isReady, setIsReady ] = useState( false ); + const handleEnterViewport = useCallback( () => setIsReady( true ), [] ); + + // The block must also render when one of its inner blocks is selected + // (e.g. reached through the List View), otherwise that block would be + // selected but never mounted for editing. + const hasSelectedInnerBlock = useSelect( + ( select ) => + select( 'core/block-editor' ).hasSelectedInnerBlock( + clientId, + true + ), + [ clientId ] + ); - if ( name !== 'core/query' ) { - return ; - } + const isActive = isSelected || hasSelectedInnerBlock; - const [ isReady, setIsReady ] = useState( false ); - const handleEnterViewport = useCallback( () => setIsReady( true ), [] ); - - // The block must also render when one of its inner blocks is selected - // (e.g. reached through the List View), otherwise that block would be - // selected but never mounted for editing. - const hasSelectedInnerBlock = useSelect( - ( select ) => - select( 'core/block-editor' ).hasSelectedInnerBlock( - clientId, - true - ), - [ clientId ] - ); + // Latch to rendered once selected so deselecting does not collapse the + // block back to a placeholder and refetch. + useEffect( () => { + if ( isActive ) { + setIsReady( true ); + } + }, [ isActive ] ); - const isActive = isSelected || hasSelectedInnerBlock; + if ( isReady || isActive ) { + return ; + } - // Latch to rendered once selected so deselecting does not collapse the - // block back to a placeholder and refetch. - useEffect( () => { - if ( isActive ) { - setIsReady( true ); - } - }, [ isActive ] ); + return ; +} - if ( isReady || isActive ) { +/** + * Defer rendering of off-screen Query Loop blocks in the editor. + * + * This HOC is registered last so it wraps the plugin's other Query Loop + * enhancements: while the placeholder is shown none of them — nor the core + * block itself — are mounted. + */ +const withViewportPlaceholder = createHigherOrderComponent( ( BlockEdit ) => { + return ( props ) => { + if ( props.name !== 'core/query' ) { return ; } - return ; + return ; }; }, 'withViewportPlaceholder' );