WordPress.org

WordPress Developer Blog

Block deprecation – a tutorial

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 all 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 in all likelihood 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 and every 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 will allow 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. 😀

At the moment 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.

2 responses to “Block deprecation – a tutorial”

  1. orionrush Avatar

    The `depricated` property provides a happy path for updating the markup of our custom block but doesn’t it still requrie that the post/page where the block is use needs to be re-saved to refresh the saved markup in the database? If I have hundreds of uses of this block across the site – how do we update them all to reflect the new content? Is manually re-saving each instance the expected solution?

    1. Michael Burridge Avatar
      Michael Burridge

      The primary purpose of block deprecation is for the editor experience, i.e. to avoid the “This block contains unexpected or invalid content” error message that could cause confusion for non-technical content editors.

      On the front end the page will show whatever content is in the database. If the new version of the block makes radical changes to the front end view and it’s imperative that the front end shows the changed version then, apart from re-saving each instance, which could indeed be laborious, you could do a search and replace across the database. Depending on the number of instances of the block across the site the time and effort involved in doing this, and the technical skills required, may or may not be worth it.

Leave a Reply

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