0% прочитано

A New UI Entry Point Should Reuse the Same Capability Registry

If a toolbar, an insert-between button, and a context menu perform the same action, they should not maintain separate lists of available types. One capability registry prevents drift, reduces duplication, and makes future UI entry points easier to add.

30 жовтня 2026 р. 8 хв читанняVue.js

An editor may already have one list of available block types: text, image, code, quote, button, and others.

At first, that list is used by a single "Add block" button. Later, a new convenience appears: insert a block between two existing blocks.

The simplest implementation may look obvious: create another list of available block types for the "insert between" button.

That is where the problem starts.

If two UI entry points perform the same action, they should share one source of truth for the capabilities that action supports.

Duplicated Capability Lists Almost Always Drift Over Time

Duplication has already created different behavior
const toolbarBlocks = [    'text',    'image',    'code',    'quote',]; const insertBetweenBlocks = [    'text',    'image',    'code',];

At first, both arrays may be identical.

A month later, someone adds a new block to the toolbar and forgets the insert-between menu. Later, one list gets a different order, icon, or availability rule.

The UI begins to look as though the product supports different capabilities in different places even though the business action is the same.

The Registry Should Describe the Capability, Not a Specific Button

It is better to keep one registry that answers: "which block types can this editor create?"

The toolbar, insert-between button, command palette, or context menu simply reads that registry and decides how to present the same capabilities in its own UI.

One registry for every way of adding a block
export const blockRegistry = [    {        type: 'text',        labelKey: 'blocks.text',        icon: TextIcon,        create: () => ({            type: 'text',            data: {},        }),    },    {        type: 'image',        labelKey: 'blocks.image',        icon: ImageIcon,        create: () => ({            type: 'image',

A New Entry Point Should Reuse the Registry Instead of Copying It

The new action uses the same block definitions
const availableBlocks =    computed(() => blockRegistry); const insertBetween = (    index,    definition,) => {    const block = definition.create();     blocks.value.splice(        index + 1,        0,        block,    );};

The new button only knows the insertion position.

It does not decide which types exist, how an initial payload is created, or which icon belongs to a block. That responsibility already belongs to the registry.

A Capability Registry Can Hold More Than a List of Types

As the editor grows, a simple type value is often no longer enough.

The registry may contain:

  • label and icon;
  • factory for initial data;
  • menu group;
  • availability condition;
  • whether the block can be inserted between other blocks;
  • whether it is allowed in a particular editor mode.

If an Entry Point Has Restrictions, Filter the Registry Instead of Forking It

An entry point may apply its own filter
const insertBetweenBlocks =    computed(() =>        blockRegistry.filter(            (block) =>                block.canInsertBetween                !== false,        )    );

This distinction matters.

An entry point may show only a subset of capabilities, but it should still start from the same canonical set.

We add a filtering rule, not a second source of truth.

The Same Action Should Share Not Only the List but the Creation Path

Even if two buttons read the same type array, duplication may remain elsewhere.

For example, the toolbar calls a factory from the registry while insert-between manually constructs the block object.

When a new initial field is added, one entry point creates the correct block while the other creates an outdated one.

Reuse should cover the capability list, creation factory, and availability rules. Otherwise duplication simply moves to another layer.

A Shared Registry Does Not Mean Identical UI

A toolbar may show icons without labels. An insert-between menu may show an icon, label, and description. A command palette may add search.

That is fine.

The shared part is the knowledge about the capability, not the way it is visually presented.

Context Rules Should Live Close to the Capability

Availability rule as part of the capability definition
{    type: 'image',    canUse: ({        settings,        readonly,    }) =>        settings.imagesEnabled        && ! readonly,}

If the "images disabled" rule is checked separately in the toolbar and separately in the insert-between menu, those checks can drift too.

It is better for both UI elements to ask the same capability definition.

What to Test

Minimal scenario coverage
1. Toolbar and insert-between use the same registry.2. Adding a new block type makes it appear in both entry points.3. A capability disabled by settings disappears from both.4. Entry-point-specific filters do not mutate the registry.5. Both entry points use the same block factory.6. Initial block data is identical regardless of where creation started.7. Reordering registry items changes both menus consistently.8. One entry point can render different UI without duplicating capability data.

This Pattern Applies Far Beyond a Block Editor

The same principle is useful for:

  • toolbar commands and command palettes;
  • table-row actions and bulk actions;
  • entity creation from a header button and an empty state;
  • context menus and primary toolbars;
  • keyboard shortcuts and normal buttons.

If all of these entry points trigger the same capability, they should reuse one shared capability definition.

A New UI Entry Point Should Not Create a New Source of Truth

When a new button or menu is added, the main question is not "which list should it show?" but "where is this capability already defined?"

If the action is the same, its capability registry, creation factory, and availability rules should remain shared.

A second way to perform the same action should be a new entry point into the existing capability, not a second implementation of that capability.

A new UI entry point should add a new way to access a capability, not a new list of truth.