Go back

Custom Invoker Commands in React: cross-component communication without context or prop drilling? ⚡️

Native Custom Invoker Commands in React: command and onCommand examples, benefits and limitations.

Custom Invoker Commands in React: cross-component communication without context or prop drilling? ⚡️

The native Invoker Commands API (command and commandfor on <button>) is already rolling out actively across browsers. It lets you open dialogs or popovers declaratively right from HTML, without a single line of JS:

<button command="show-modal" commandfor="my-dialog">Открыть</button>
<dialog id="my-dialog">...</dialog>

But Sergio Xalambrí’s article explores a much more interesting scenario: Custom Invoker Commands.

How does it work in React?

Instead of built-in commands such as show-modal, we can pass a custom command (for example, command="--custom-action"). The browser itself sends a custom command event to the target element.

In React, we can attach a regular onCommand listener to the target DOM element:

function ActionButton({ targetId }: { targetId: string }) {
  return (
    <button command="--reset-counter" commandfor={targetId}>
      Сбросить счётчик
    </button>
  );
}

function Counter({ id }: { id: string }) {
  const [count, setCount] = useState(0);

  return (
    <div
      id={id}
      onCommand={(e) => {
        if (e.command === "--reset-counter") {
          setCount(0);
        }
      }}
    >
      <p>Счётчик: {count}</p>
    </div>
  );
}

The button and receiving element can live in completely different branches of the React tree. Passing the command requires neither global state, nor Context API, nor prop drilling.

➕ Benefits of the approach

  1. Decoupling: The action button knows absolutely nothing about the target component’s state. It only needs the DOM node’s id.
  2. Fewer rerenders: Unlike React Context or Redux/Zustand, clicking such a button does not trigger cascading rerenders of parent components or the context tree. It uses native browser event dispatching.
  3. Works before hydration (zero JS to send): Clicking the button generates a DOM-level event even while the page is loading. Once React hydrates the target component and attaches onCommand, the handler runs.
  4. Standardization: This is not a workaround or a React-specific pattern, but a native, specified web standard.

➖ Drawbacks and bottlenecks

  1. Tied to IDs: commandfor requires a fixed link to a DOM element’s id. In React components with dynamic id values (for example, through useId()), you need to make sure identifiers are passed correctly.
  2. TypeScript & React types: Because the specification is new, React types (ButtonHTMLAttributes) and the JSX onCommand event may require manual augmentation (declare module 'react') in some projects.
  3. Strict syntax: Custom command names must start with two hyphens (--my-command), or the browser will ignore the command or throw an error.

What do you think of using native browser events instead of React Context for simple UI commands?


Read the article

I also recommend reading the article about implementing a Markdown toolbar in React using this approach: https://sergiodxa.com/tutorials/build-a-command-driven-markdown-toolbar-in-react

Copied