How to Manage Block Deprecations with Static WordPress Blocks | Block Creator 03

Welcome to part 03 of the Block Creator series!

In this lesson we’ll explore a more complex topic of block deprecations. This is one of the more challenging aspects of static blocks. Don’t worry though, after this lesson we’ll transition to dynamic blocks, which will feel much more familiar to traditional WordPress.

Note: This post is part of the Block Creator series. These lessons build on each other, so it is recommended to complete them in order to get the most out of the course.

You can find links to all the Block Creator lessons on the main course page. You can also download this finished code from the course Github repo in case you get stuck at any point.

Understanding block validation errors

Throughout the past couple lessons you may have encountered this error at some point:

custom block validation error

This is called a block validation error and is unique to static blocks. Here’s when/why it happens:

  1. You insert a block into a post
  2. You edit the markup of the block’s save function, or change the name of an attribute associated with the block.
  3. You return to the editor and reload the page

Behind the scenes WordPress loads the markup (and data attributes) for the block from the database. It runs the current save function against the data to validate that the block and database data are in sync. When the save function has been changed, these no longer match and WordPress raises an error.

This is actually a good thing as it keeps data from getting corrupted or lost, but it can be frustrating when developing blocks. Something as simple as adding an extra class or wrapping div will cause all your blocks to show this nasty alert.

During the active development process you can always just delete and re-insert, but what about if you’ve developed a site for a client, and then they come back with a modification request to a block that already exists on several pages?

We need a way to update blocks without breaking all the existing instances of the block on a site.

Introducing block deprecations

WordPress has a mechanism for this called block deprecations. The basic idea is as follows:

Before we update the save function with a modification, we take the current save function and save it as a deprecated version. We then provide our block with an array of any deprecated save functions (from most recent to oldest).

Now we make our modifications. When WordPress attempts to load an existing version of the block the validation will fail because of our changes.

Instead of throwing the alert, however, WordPress will now compare against each save function in our array of deprecations. In this case, the most recent deprecated function would match and the block would successfully validate and render in the editor.

Only if the block fails to validate against all the existing deprecations (for instance if we made a change and forgot to add the old save function to our deprecations array), would WordPress throw an alert.

Implementing Deprecations for our block

Let’s see this in action with our super simple banner block.

For our example, let’s assume a client wants to change the banner block so instead of using large H2 text elements, it uses a paragraph tag.

First, create a new page and add an instance of the basic static banner block you created from lesson 01 and lesson 02. Go ahead and save/publish the page.

Now open the new page in a fresh tab so that you can view your block in the editor and on the frontend.

Go ahead and run npm run start so that our block will compile in the background.

First let’s update our block.json file to change the selector option to a paragraph tag instead of an H2 element:

	"attributes": {
		"content": {
			"type": "string",
			"source": "text",
			"selector": "p"
		}
	},

Next, we’ll modify our edit.js file and save.js file to make the requested change. In both we simply change the tagName property of our RichText element:

export default function Edit({attributes, setAttributes}) {

	const blockProps = useBlockProps({className: "basic-block"})

	return (
		<div { ...blockProps }>
			<RichText
					tagName="p" 
					value={ attributes.content } 
					allowedFormats={ [ 'core/bold', 'core/italic' ] } 
					onChange={ ( content ) => setAttributes( { content } ) } 
					placeholder={ __( 'Enter your banner text...' ) } 
			/>
		</div>
	);
}
export default function save({attributes}) {
	const blockProps = useBlockProps.save({
		className: "basic-block"
	})
	return (
		<div { ...blockProps }>
				<RichText.Content 
					tagName="p"
					value={attributes.content}
				/>
		</div>
	);
}

At this point, if you refresh the editor you will see the validation error on the old block:

Notice however, this only affects the editor, the front end still displays fine. This is part of the idea of the validation process: to catch potential errors in the editor and protect the front end of the site.

Leave the broken block in place so that we can go through implementing a deprecation fix.

Protecting existing blocks from the validation error

To prevent this error from showing we need to add a deprecated save function of how our block looked previously. Normally, I would do this first before modifying a block, but I wanted to show the block breaking for demonstration purposes.

Create a new file within the src folder of your block called deprecated.js

We’ll export an array from this file that represents the previous versions of a block. First let’s create an object called v1 to capture the previous state with the H2 element.

Within the object we need to provide the old save function, as well as a snapshot of the attributes from this block version, even if they have not changed.

import { useBlockProps, RichText } from '@wordpress/block-editor';

const v1 = {
	attributes: {
		content: {
			type: "string",
			source: "text",
			selector: "h2"
		}
	},
	save: ({attributes}) => {
		const blockProps = useBlockProps.save({
			className: "basic-block"
		})
		return (
			<div { ...blockProps }>
				<RichText.Content 
					tagName="h2"
					value={attributes.content}
				/>
			</div>
		);
	}
}

export default [v1]

Now, we just need to import this into our index.js file and pass it into the ‘deprecated’ option (which takes an array) when we register our block:

import Edit from './edit';
import save from './save';
import metadata from './block.json';

import deprecated from './deprecated'

registerBlockType( metadata.name, {
	edit: Edit,
	save,
	deprecated
} );

Perfect. Go ahead and go back to the editor and refresh. The block validation error should be gone and the text should now be a paragraph tag!

If you refresh the front end of the site before re-saving the post though, you’ll notice it still shows an H2. This is an important limitation of static blocks.

You cannot update them across the site programmatically (without using a database search and replace). To actually change the existing blocks we have to edit the post they reside in and click update manually.

We’ll address this further at the end of the lesson, but let’s run through one more deprecation example with our block first. If you haven’t already, go ahead and click update so that your block now shows the paragraph tag on the front end as well.

Deprecating and changing attributes on a block

What if another change request comes through to allow a block to have a heading (h2) and accompanying body text (p)?

We’ll do the process in a different order this time to show how I would approach this in the real world.

First, we’ll capture a snapshot of our current block in the deprecated.js file and assign it to a variable named v2. Don’t forget to add it to the export array as well. Best practice is to export the most recent first, since it is most likely to match when WordPress checks against all deprecations.

const v2 = {
	attributes: {
		content: {
			type: "string",
			source: "text",
			selector: "p"
		}
	},
	save({attributes}) {
    const blockProps = useBlockProps.save({
      className: "basic-block"
    })
    return (
      <div { ...blockProps }>
          <RichText.Content 
            tagName="p"
            value={attributes.content}
          />
      </div>
    );
  }
}

// v1 version here...

export default [v2, v1]

Now we can proceed to make our changes. First, let’s update the block.json file to address our new heading attributes.

I’ve added a “heading” attribute and renamed the “content” attribute to “body” to be more descriptive:

	"attributes": {
		"body": {
			"type": "string",
			"source": "text",
			"selector": "p"
		},
		"heading": {
			"type": "string",
			"source": "text",
			"selector": "h2"
		}
	},

Don’t forget to make sure you have the correct the selector properties for the heading/body.

Next, let’s update our edit and save functions to support the new field:

export default function Edit({attributes, setAttributes}) {

	const blockProps = useBlockProps({className: "basic-block"})

	return (
		<div { ...blockProps }>
			<RichText
					tagName="h2" 
					value={ attributes.heading } 
					allowedFormats={ [ 'core/bold', 'core/italic' ] } 
					onChange={ ( heading ) => setAttributes( { heading } ) } 
					placeholder={ __( 'Enter your banner text...' ) } 
			/>
			<RichText
					tagName="p" 
					value={ attributes.body } 
					allowedFormats={ [ 'core/bold', 'core/italic' ] } 
					onChange={ ( body ) => setAttributes( { body } ) } 
					placeholder={ __( 'Enter your banner text...' ) } 
			/>
		</div>
	);
}
export default function save({attributes}) {
	const blockProps = useBlockProps.save({
		className: "basic-block"
	})
	return (
		<div { ...blockProps }>
				<RichText.Content 
					tagName="h2"
					value={attributes.heading}
				/>
				<RichText.Content 
					tagName="p"
					value={attributes.body}
				/>
		</div>
	);
}

There is one final step we need to do because we are renaming the “content” attribute to “body” instead.

We need to tell WordPress how to handle this in our V2 deprecation. WordPress provides a method called “migrate” for this purpose:

const v2 = {
	attributes: {
		content: {
			type: "string",
			source: "text",
			selector: ".basic-block"
		}
	},
// New method
  migrate({content}){
    return {
      body: content
    }
  },
// End new method
	save({attributes}) {
    const blockProps = useBlockProps.save({
      className: "basic-block"
    })
    return (
      <div { ...blockProps }>
          <RichText.Content 
            tagName="p"
            value={attributes.content}
          />
      </div>
    );
  }
}

Migrate accepts the saved legacy attributes that we can de-structure and assign to our updated attributes in an object. We return this object that acts as a key to render the updated block in the editor. In this case, we just need to pass whatever we stored as “content” into our new “body” attribute.

You can now go ahead and refresh the editor.

This time, you won’t see any deprecation error. The block will be ready to go with a new heading field above the existing paragraph text! This is because we implemented our v2 deprecation in advance of making the changes to the save function.

To see our changes on the front end, we will need to edit our title and hit update.

Whew… Okay we are done with our deprecation segment. Let’s zoom out and discuss some issues with the static blocks we’ve been working with.

Static block limitations

Static blocks are the foundational building blocks of Gutenberg and are extremely performant. Beyond very simple foundational blocks they start to have some limitations though.

You already saw that the deprecation process is quite involved. When I first learned block deprecations my immediate thought was that there was no way I would be able to manage this across a library of blocks on client sites.

Even if you look past the amount of work required just to make simple structural changes to published blocks – the bigger issue is that updates will not automatically apply where blocks exist already.

This is a huge issue on sites with a large volume of pages. It can become completely unmanageable to manually go through and re-save posts to update blocks.

Introducing dynamic blocks

The solution to this issue is dynamic blocks. Dynamic blocks only store an object that contains the attributes for a given block in the database without any markup. When WordPress loads dynamic blocks, it renders a PHP template and plugs in the attribute values on each request.

What this means is that if you change the PHP render template, it will immediately reflect those changes everywhere that block is used. No need for deprecations, and no need to manually resave a page.

This is truly game changing and makes blocks behave more like traditional short codes with super powers.

I believe that understanding static blocks is important foundational knowledge on how the block editor works, but for the rest of this course we will be building almost exclusively dynamic blocks, as they are infinitely more practical for most client projects.

Wrap up

Congratulations!

Block deprecations are a tricky topic to wrap your head around. The good news is: you won’t need to use them again for the rest of the course!

In the very next lesson we’ll dive into creating dynamic blocks. If you come from PHP, this will seem like a breath of fresh air after this lesson. We’ll also set aside the somewhat contrived fictional banner block from the last few lessons, and start creating some real world blocks that I use on sites all the time.

Leave a Reply

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