Sometimes, you need to store unique data to a block to identify it later on. So you need to prevent having duplicate block attributes. In my case, I want to store a unique block ID. For this the clientId is useful as it’s already unique per definition (it’s a universally unique identifier generated on every editor load for every block).

However, since it’s auto-generated on every editor load, a block has a different clientId every time it’s being loaded. So we need to store this data somewhere else. In this case, as simple attribute:

const attributes = {
	blockId: {
		type: 'string',
	},
};
Code language: JavaScript (javascript)

Now when it comes to save the attribute, we must make sure that we also set a new value (which is the auto-generated clientId) if the attribute is not yet set. Otherwise we would end up in a new ID every time.

if ( ! blockId ) {
	setAttributes( { blockId: clientId } );
}
Code language: JavaScript (javascript)

To make sure it only runs once while rendering the component, we can wrap it by a useEffect with an empty array as second parameter to tell React that it has no dependency and only needs to run once:

import { useEffect } from '@wordpress/element';

useEffect( () => {
	if ( ! blockId ) {
		setAttributes( { blockId: clientId } );
	}
}, [] );
Code language: JavaScript (javascript)

This works fine in the edit function of my block, but now multiple blocks can have the identical block ID, e.g. when you manually copy and paste them into the same editor. To prevent this, we need to check every block if the identical block ID is already in use and if that’s the case, update it to its current clientId:

import {
	store as blockEditorStore,
} from '@wordpress/block-editor';
import { useEffect } from '@wordpress/element';
import { select } from '@wordpress/data';

const isBlockIdInUse = ( clientId, blockId, blocks ) => {
	let hasBlockId = false;
	
	for ( const block of blocks ) {
		if ( block.clientId === clientId ) {
			continue;
		}
		
		if ( block.innerBlocks ) {
			hasBlockId = isBlockIdInUse( clientId, blockId, block.innerBlocks );
		}
		
		if ( hasBlockId ) {
			return true;
		}
		
		hasBlockId = block.attributes.blockId === blockId;
		
		if ( hasBlockId ) {
			return true;
		}
	}
	
	return false;
}
	
useEffect( () => {
	if ( ! blockId ) {
		setAttributes( { blockId: clientId } );
		
		return;
	}
	
	const allBlocks = select( blockEditorStore ).getBlocks();
	
	if ( isBlockIdInUse( clientId, blockId, allBlocks ) ) {
		setAttributes( { blockId: clientId } );
		
		return;
	}
}, [] );
Code language: JavaScript (javascript)

As you can see, we first introduce the new function isBlockIdInUse, which checks all given blocks recursively for existence of the given block ID, except for the current block. Then this check is added to our useEffect function.

This works fine for a regular block and if you copy and paste it or doing similar things in the edit function of the block.

Unfortunately, it doesn’t work when you duplicate the block via toolbar or do other manipulations to it outside of the block edit scope, since the edit function simply doesn’t run there – or at least the useEffect doesn’t.

Fortunately, there is a filter, which can be used to achieve that. Via editor.BlockEdit filter we can use basically the identical functionality, but it also runs by duplicating a block via toolbar:

import {
	store as blockEditorStore,
} from '@wordpress/block-editor';
import { createHigherOrderComponent } from '@wordpress/compose';
import { useEffect } from '@wordpress/element';
import { select } from '@wordpress/data';
import { addFilter } from '@wordpress/hooks';

const setBlockId = createHigherOrderComponent( ( BlockEdit ) => ( props ) => {
	const {
		attributes: {
			blockId,
		},
		clientId,
		setAttributes,
		name,
	} = props;
	
	if ( name !== 'my-block/block' ) {
		return <BlockEdit { ...props } />;
	}
	
	const isBlockIdInUse = ( clientId, blockId, blocks ) => {
		let hasBlockId = false;
		
		for ( const block of blocks ) {
			if ( block.clientId === clientId ) {
				continue;
			}
			
			if ( block.innerBlocks ) {
				hasBlockId = isBlockIdInUse( clientId, blockId, block.innerBlocks );
			}
			
			if ( hasBlockId ) {
				return true;
			}
			
			hasBlockId = block.attributes.blockId === blockId;
			
			if ( hasBlockId ) {
				return true;
			}
		}
		
		return false;
	}
	
	useEffect( () => {
		if ( ! blockId ) {
			setAttributes( { blockId: clientId } );
			
			return;
		}
		
		const allBlocks = select( blockEditorStore ).getBlocks();
		
		if ( isBlockIdInUse( clientId, blockId, allBlocks ) ) {
			setAttributes( { blockId: clientId } );
			
			return;
		}
	}, [] );
	
	return <BlockEdit { ...props } />;
} );

addFilter( 'editor.BlockEdit', 'myBlock/block-id-update/set-block-id', setBlockId );
Code language: JavaScript (javascript)

This filter is limited to my block with the name my-block/block, so you definitely need to adjust that to your block’s name.

Leave a Reply

Your email address will not be published. Required fields are marked *