npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

block-editor-hmr

v1.0.0

Published

Utilities to autoload and hot-reload WordPress Block Editor modules.

Downloads

5,137

Readme

Hot-Reloading Utilities for the WordPress Block Editor

This library aims to make hot-reloading Gutenberg editor blocks & plugins as simple as possible.

What does this solve?

When you run a --hot DevServer with @wordpress/scripts, you may experience errors when a hot update tries to re-register an already-registered block. Removing or changing block registrations can also lead to unexpected errors if a block's editing interface is visible within the Editor area, and hot updates may not immediately be reflected in the editor until a user action prompts a re-render.

This library provides a utility which will deregister outgoing blocks as they hot-reload, allowing the new version to be restored properly. It also manages selection state, so that your selected block is reopened after the editor and any changed blocks are visually refreshed.

Hot-Reloading Boilerplate

To use this library, a small snippet of code is required in the file where you register your custom block.

Assuming your blocks are stored in a folder organized like this:

src
└── blocks
    ├── block-a
    │   ├── block.json
    │   └── index.js
    ├── block-b
    │   ├── block.json
    │   └── index.js
    └── block-c
        ├── block.json
        └── index.js

and that you are building your project into per-block bundles using wp-scripts build, you can require this package within a JS module's if ( module.hot ) { ... } block to facilitate hot-reloading without "block already registered" errors or jumps in the editor due to changes in what block is selected.

The standard boilerplate expected by this utility can be seen at the bottom of this example block index.js:

import { registerBlockType } from '@wordpress/blocks';

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

import './style.scss';

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

// Block HMR boilerplate.
if ( module.hot ) {
	module.hot.accept();
	const { deregisterBlock, refreshEditor } = require( '../../helpers/hot-blocks.js' );
	module.hot.dispose( deregisterBlock( metadata.name ) );
	refreshEditor( metadata.name, module.hot.data );
}

API

deregisterBlock( blockName: string, variants: ?object )

The deregisterBlock function returns a callback which should be passed to module.hot.dispose(), which cleans up the outgoing block before the new version gets registered. It takes the name of a block (required), and an optional object of block filters and styles to unhook.

Hot-swapping block styles and block editor filters

This second variants argument can be used when your block sets up JS-side block styles, filter hooks, or block variations on initialization to avoid double-registering styles or filters when re-registering the new version of the block and its variants after a hot reload. For example,

// This is the bottom of index.js, after the normal block registration
// boilerplate as shown in the example above.

// This block declares several block styles and block variations. We need to
// unhook each one before registering the new versions on reload.
const styles = [
	{
		name: 'light',
		label: __( 'Light', 'textdomain' ),
		isDefault: true,
	},
	{
		name: 'dark',
		label: __( 'Dark', 'textdomain' ),
	},
];

styles.forEach( ( style ) => registerBlockStyle( metadata.name, style ) );

const variations = [
	{
		name: 'cta',
		title: __( 'CTA Button', 'textdomain' ),
		description: __( 'The major call-to-action on the page', 'textdomain' ),
		attributes: {
			backgroundColor: 'primary',
			fontSize: 'large',
		},
		isDefault: false,
	},
];

variations.forEach( variation ) => registerBlockVariation( metadata.name, variation );

// Block HMR boilerplate.
if ( module.hot ) {
	module.hot.accept();
	const { deregister, refresh } = require( '../../helpers/hot-blocks.js' );
	// Pass the style and variations arrays when de-registering to dispose of them correctly.
	module.hot.dispose( deregisterBlock( metadata.name, { styles, variations } ) );
	refreshEditor( metadata.name, module.hot.data );
}

This can object can also be used to pass an array of filters, defined as an array of objects specifying the hook name, namespace string, and callback function. Here's an example of how you'd define your filters using a hooks array that can be passed to deregisterBlock() later:

const hooks = [
	{
		hook: 'blocks.registerBlockType',
		namespace: 'my-plugin/class-names/list-block',
		callback: addListBlockClassName
	},
];

hooks.forEach( ( { hook, namespace, callback } ) => {
	wp.hooks.addFilter( hook, namespace, callback );
} );

refreshEditor( blockName: string, data: object )

The refreshEditor function in the HMR boilerplate above handles resetting the editor state appropriately after the new version of a block comes in. If a block is deregistered and then re-registered, it will lose focus in the editor. If the edit method for that block changes its UI on isSelected, this can cause unexpected layout shifts and prevent easy iteration on edit method subcomponents.

Hot-reloading will work without it, but it should be a smoother experience if you maintain this in your HMR boilerplate.

Script Dependencies

For this to work, the bundle which utilizes these methods must be enqueued specifying wp-blocks, wp-hooks, and wp-data as script dependencies.

How does it work?

If we try to register a block without unregistering it first, the block editor throws an error and refuses to process the newer version of the block. We therefore unregister each hot-block at the start of the HMR update cycle (within module.hot.dispose(), when the outgoing version is being processed) and then register the new block in its place.

Can we simplify that boilerplate?

It's possible this could be simplified further, but testing to date indicates that module.hot.accept must be called within the module file within your project, rather than being abstracted within the third-party NPM module.

What if I do not use per-block bundles?

While we recommend following wp-scripts' preferred structure and bundling your JS at the level of an individual block (for both performance and maintainability reasons), if you do still use a kitchen-sink bundle for all of your blocks, you may prefer to stick with the legacy v0.7.0 API documented here.