3

I have a custom Gutenberg block that contains a list of InnerBlocks (all the same known block), and I am trying to output the title attribute from each of those blocks in a list on the parent block.

I'm trying to get values from the innerblocks and add them to an array attribute on the parent block,

This is a simplified version of the parent block edit.js

const Edit = ( props ) => {
    const { attributes: { childValuesArray }, setAttributes } = props;
    const blockProps = useBlockProps();
    const ALLOWED_BLOCKS = [ 'custom/child-block' ];

    const { clientId } = props;
    const { select } = wp.data;
    const parentBlock = select( 'core/block-editor' ).getBlocksByClientId( clientId )[ 0 ];
    const childBlocks = parentBlock.innerBlocks;

    childBlocks.map(block => {
        childValuesArray.push( block.attributes.title, block.clientId );
    });
    props.setAttributes({ 'childValuesArray': childValuesArray });

    return (
        <div { ...blockProps }>
            <div className="block-content">
                <InnerBlocks allowedBlocks={ ALLOWED_BLOCKS } />
            </div>
            <aside>
                {
                    childValuesArray.map( (block, index) => {
                        return (
                            <p id={ block.clientId }>
                                { block.title }
                            </p>
                        )
                    })
                }
            </aside>
        </div>
    );
};

export default Edit;

The issue I'm having is that the code to grab the child block values runs continuously, so the array of values keeps getting bigger.

Research suggests that I should maybe be using withDispatch to avoid the code running every time, but the documentation is rather brief, and I haven't been able to work out how to do this correctly.

Is withDispatch the right approach here? If so, how do I go about using it? If not, what's a better approach?

1 Answer 1

3

It's worth a mention that storing child block attributes in the parent block's attributes is a bit of a bizarre practice in isolation - ideally blocks should be completely independent of one-another. But if some functionality necessitates that a parent block access a child block's attributes, it would likely have a much smaller potential for headaches down the road to just read the attributes from the child blocks as necessary rather than duplicating the data into parent block attributes.

In short, you need to create a subscription which will re-render the component when changes occur in editor state by using either the useSelect() hook or the withSelect() HOC factory (I've written a brief introduction to the concepts behind these functions over here).

While a subscription to getBlocksByClientId( parent_block_client_id ) would be perfectly functional as the parent block's state becomes "dirty" when any change is made is made to a child block, I think a getBlocks( parent_block_client_id ) selector is a little cleaner as you don't have to traverse down to the innerBlocks array, and additionally the subscription won't trigger a re-render when any modification is made to the parent block.

In order to reduce overhead, it would be a good practice to place your logic which updates your attributes in a useEffect() hook with a dependency on the child block list such that it only occurs when the child blocks change.

import { useBlockProps, InnerBlocks } from '@wordpress/block-editor';
import { useSelect } from '@wordpress/data';
import { useEffect } from '@wordpress/element';

const Edit = ( { attributes, clientId, setAttributes, ...props } ) => {
  const { child_values } = attributes;
  const child_blocks = useSelect(
    select => select( 'core/block-editor' ).getBlocks( clientId )
  );

  useEffect(
    () => {
      const child_values = child_blocks.map(
        ( { clientId, attributes: { title } } ) => ( { clientId, title } )
      );
      setAttributes( { child_values } );
    },
    [ child_blocks ]
  );

  return (
    <div {...useBlockProps()}>
      <div className="block-content">
        <InnerBlocks />
      </div>
      <aside>
      {child_values.map( ( { clientId, title }, index ) => (
        <p id={ clientId } key={ index }>{title}</p>
      ))}
      </aside>
    </div>
  );
}

export default Edit;
1
  • Fantastic response, thank you. Solved my problem and gave a really clear explanation of what I should be doing and why. I appreciate the linked explanation of useSelect() and withSelect() as well - that's exactly the sort of thing I had been hoping to find when I was digging through the documentation!
    – Infobahn
    Jun 4, 2022 at 12:54

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.