Consider this scenario. You’ve created a plugin that implements a static block, but now you’ve had a great idea for an update, an idea that will make the block you’ve developed just so much better. The problem is that it’s been published to the WordPress plugin directory and the block is in use on hundreds of websites.
Now consider this scenario. Your client has just been in touch, they want some changes made to the custom block that you developed for their website. The problem is that it’s a static block and the block is used in dozens, if not hundreds, of places in various pages and posts across their site.
Why are these scenarios a problem for you as a block developer?
If you make a change to a static block, and specifically if you make a change to the save()
function, i.e., the function that determines what content gets saved to the database and ultimately gets rendered in the front end, then you will see the “This block contains unexpected or invalid content” error message when the page is next loaded in the editor after the change.
This occurs because the structure of the content previously saved by the block does not match the structure that the new version will save and so you get a block validation error. This is a very common occurrence during the development of a block, and developers working with blocks are very aware of this and are used to dealing with it.
However, you wouldn’t want users to see this error message as they will likely be unaware of the reasons underlying it and hence will think that something has gone wrong, which in a sense it has!
If you make a change to your block then content editors and other users will see this validation error in each instance of the block across their site until they hit the “Attempt Block Recovery” button for each occurrence of the block across the site.
So in the first scenario above you risk annoying potentially hundreds of users of the block, and in the second you risk upsetting your client, or at least the content editors responsible for their website, who will need to go through the entire site and recover all the instances of the changed block.
So what do you do? Discard that great idea? Tell the client that the change can’t be made? Make it a dynamic block instead of a static block? Create a brand-new block and tell your users to use the new alternate version instead?
No, of course not! None of these is necessary.
The solution – block deprecation
What you do is you “deprecate” the old version of the block. Deprecation provides a graceful fallback for instances of the block based on the earlier version. In this way content editors will never see the “This block contains unexpected or invalid content” error message, but whenever they update a page or post that contains your block then your block will save the updated version to the database, replacing the content saved by the old version.
Likewise, if they’re creating a new instance of the block the latest and most up-to-date version of the content will be saved.
Before digging into how to deprecate a block let’s pause for a moment to consider the scope of this problem.
This is only a problem for static blocks, and it’s only a problem for the editor.
Static blocks have a save()
function that saves content to the post. That saved content is subject to validation. On the other hand, dynamic blocks do not have a save()
function. Their content is rendered in real-time, so it is not saved to the database and hence not subject to validation. You will therefore never get the “This block contains unexpected or invalid content” error message with a dynamic block. To learn more about dynamic blocks, how they differ from static blocks, and when you should use one or the other, read Joni Halabi’s article on this topic.
And this is only a problem in the editor, the front-end remains unaffected by any changes to the block. A person viewing the front-end of the website will continue to see whatever is stored in the wp_posts
table of the database even if it’s been saved using an older version of the block and hasn’t been updated using the newer version.
So let’s look at some examples of block deprecation.
Block deprecation – a simple example
Scaffold a new block plugin with @wordpress/create-block
:
npx @wordpress/create-block deprecation-example
Activate the new plugin in the WordPress admin and create a post that uses the new block.
This is what you’ll see in the editor:
And this is what you’ll see in the front-end:
Now let’s make a change to the block. Remember to run npm start
from within the wp-content/plugins/deprecation-example
directory first. Note that all the changes in this tutorial are made to files contained in the src
directory.
Change the content returned by the save()
function in save.js
. For example change line 21 from:
{ 'Deprecation Example – hello from the saved content!' }
to:
{ 'Deprecation Example – hi from the saved content!' }
Save the file and reload the page in the editor and in the front-end. The front-end won’t change, it is after all just rendering whatever is stored in the database and that hasn’t changed.
In the editor, however, you will see precisely what you don’t want content editors to see, namely the “This block contains unexpected or invalid content.” error and if you look in the browser console you’ll see something like this:
Woah – hold on! ✋ Don’t attempt to recover the block by clicking on the “Attempt Block Recovery” button. Let’s instead fix it by deprecating the block.
Create a file called deprecated.js
in the src
directory. In the file create a const
, call it v1
, and give it a value of an object containing the old version of the save()
function:
const v1 = {
save() {
return (
<p { ...useBlockProps.save() }>
{ 'Deprecation Example – hello from the saved content!' }
</p>
);
}
}
Because this is using the useBlockProps
hook you’ll need to import that into deprecated.js
too:
import { useBlockProps } from '@wordpress/block-editor';
Finally, in deprecated.js
export an array containing just one item, namely the object you just created:
export default [ v1 ];
Import the array into index.js
and reference it in the configuration object passed to the registerBlockType
function call:
import deprecated from './deprecated';
registerBlockType( metadata.name, {
edit: Edit,
save,
deprecated
} );
Save the changed files, and now if you refresh the page in the editor the block validation error disappears. And if you update the post (you may need to add a new block or make some other change to the page) then the new version of the content will appear in the front end.
That’s block deprecation in action – a content editor would be completely unaware that there had been any changes to the block.
So let’s explain what just happened. When the block editor determined that the saved content wasn’t a match for the version that would be generated by the save()
function, it looked for a deprecated
property in the block configuration object passed to the registerBlockType()
function. It then traversed the array of objects (there’s currently only one) looking for one that had a save()
function that did match the saved content. This was then used to parse the block in the editor and then the new version is used when the post or page is updated and the block saves the newer version of the content to the database.
Iterating the example with block attributes
Now let’s try out a slightly more complicated example. First up, add an attributes
object to block.json
and add a text
property:
"attributes": {
"text": {
"type": "string",
"source": "html",
"selector": "p",
"default": "Deprecation Test"
}
}
This allows the text to be user configurable rather than hard-coded. To make it user configurable you’ll need to update edit.js
.
Firstly, import the RichText
component into edit.js
:
import { useBlockProps, RichText } from '@wordpress/block-editor';
Then update the Edit()
function to use it:
export default function Edit( { attributes, setAttributes } ) {
const onChangeContent = ( val ) => {
setAttributes( { text: val } )
}
return (
<RichText { ...useBlockProps() }
tagName="p"
onChange={ onChangeContent }
value={ attributes.text }
placeholder="Enter text here..."
/>
);
}
Remember to destructure attributes
and setAttributes
into the function’s parameter list. setAttributes
is used in the onChange
function to update the text
attribute.
The text is now editable. Go on, try it. Now for the crucial bit – deprecate the previous version and update save.js
.
In deprecated.js
add a new const
and call it v2
. Set its value to an object containing the current version of the save()
function:
const v2 = {
save() {
return (
<p { ...useBlockProps.save() }>
{ 'Deprecation Example – hi from the saved content!' }
</p>
);
}
}
Now import the RichText
component into save.js
:
import { useBlockProps, RichText } from '@wordpress/block-editor';
And update the save()
function:
export default function save( { attributes } ) {
return (
<RichText.Content { ...useBlockProps.save() }
tagName="p"
value={ attributes.text }
/>
);
}
The final thing to do is add the v2
const to the array exported by deprecated.js
:
export default [ v2, v1 ];
Note that it’s good practice to put the most recent revision first in the array. The array is parsed starting at the first element until a matching version is found, so doing this means that the most recent deprecation is tried first. As, generally speaking, the most recent deprecation is the most likely to provide a match, putting it first in the array avoids unnecessarily processing deprecations that are less likely to match.
Migrating attributes
Now let’s look at doing something else that’s often necessary when making changes to a block, namely migrating attributes. The block currently has an attribute called text
, but suppose we want the new version to use content
as the attribute name as text
is going to be used for another purpose, and content
is in any case a more meaningful name for the, erm…, content of the block.
The first thing to do is to ensure that a copy is made of the current version of the save()
function in deprecated.js
. Add a new const
called v3
and assign it a value of an object containing the most recent version of the save()
function:
const v3 = {
save( { attributes } ) {
return (
<RichText.Content { ...useBlockProps.save() }
tagName="p"
value={ attributes.text }
/>
);
}
}
As this version uses the RichText
component you’ll also need to import RichText
into deprecated.js
:
import { useBlockProps, RichText } from '@wordpress/block-editor';
Next, over in block.json
, rename the attribute changing it from text
to content
:
"attributes": {
"content": {
"type": "string",
"source": "html",
"selector": "p"
}
}
Now we need to change every reference to text
in both the Edit()
function and the save()
function to instead refer to content
. Here’s the new Edit()
function, there’s two references to change – don’t forget about the one in the onChange
handler:
export default function Edit( { attributes, setAttributes } ) {
const onChangeContent = ( val ) => {
setAttributes( { content: val } )
}
return (
<RichText { ...useBlockProps() }
tagName="p"
onChange={ onChangeContent }
value={ attributes.content }
placeholder="Enter text here..."
/>
);
}
And here’s the new save()
function where there’s just one reference to change:
export default function save( { attributes } ) {
return (
<RichText.Content { ...useBlockProps.save() }
tagName="p"
value={ attributes.content }
/>
);
}
Now we need to ensure that the deprecation knows about the attribute and migrates it to the new name. Add an attributes
property to the v3
object with the to-be-deprecated text
attribute:
attributes: {
text: {
type: 'string',
source: 'html',
selector: 'p',
},
},
Next add a migrate
function, destructuring the text
attribute from the passed object. The function should return the new attribute, namely content
, with the value that was previously in text
:
migrate( { text } ) {
return {
content: text,
};
},
We’re only migrating attributes here, but the migrate
function can also be used to migrate innerBlocks
.
In the end your v3
object should look like this:
const v3 = {
attributes: {
text: {
type: 'string',
source: 'html',
selector: 'p',
},
},
migrate( { text } ) {
return {
content: text,
};
},
save( { attributes } ) {
return (
<RichText.Content { ...useBlockProps.save() }
tagName="p"
value={ attributes.text }
/>
);
}
}
It remains to add the v3
object to the head of the array:
export default [ v3, v2, v1 ];
Now when you refresh the editor page containing your block the text that was previously in the text
attribute is now in the content
attribute. You can confirm this by selecting the block in the editor and running this command in the browser’s console:
wp.data.select( 'core/block-editor' ).getSelectedBlock().attributes
Wrap-up
So, to briefly summarize, the block configuration object passed to the registerBlockType()
function can have a deprecated
property which takes as its value an array. Each deprecated version of a block is an object in that array, and each object can have the following properties:
attributes
supports
save
migrate
isEligible
For more information on block deprecation, including the supports
and isEligible
properties not covered in this post, see the Deprecation page in the block editor handbook.
It’s also a good idea to look at some examples of deprecated blocks in the Gutenberg repository. The deprecated.js file in the Cover block is a good example to model your own deprecation on. Take a look also at the one in the Button block. This one is harder to read and demonstrates why it’s a good idea to save each version in a const
and add that const
to the exported array – it looks cleaner, is easier to read, and is easier to update with more deprecations as time goes on and your block continues to evolve.
A challenge
Did you think you’d get to the end of this post without having some homework to do? Well, think again. 😀
Currently, the RichText
component renders a <p>
element in the markup, as can be seen if you inspect the element in the front end:
Your challenge is to change the block so that it instead renders a <div>
element. Deprecate the version that renders the <p>
element. Good luck!
[If you get stuck with this challenge, see the solution in this gist]
Props to @bph, @fabiankaegy, @thatdevgirl, and @greenshady for reviewing this post and making it better than it otherwise would have been.
Leave a Reply