Skip to content
HomePagesHomePages template kit

require-editor-reason

"Every fork on `inCanvas` must carry an `editor-reason:` comment naming what it protects."

Venue: eslint — Every fork on inCanvas must carry an editor-reason: comment naming what it protects.

Every place your island forks its behavior on inCanvas must carry a comment directly above it opening with editor-reason:, naming the published-only behavior the fork protects.

// editor-reason: the lightbox is a fullscreen scroll-locking takeover that would cover
// the page the author is editing.
if (inCanvas) return;

The comment sits on the line(s) immediately above — a blank line between breaks it. Both // and {/* … */} count, so the marker works in JSX children as well as in a handler or an attribute list. Where a block of comment lines sits above the fork, the first line carries the marker.

Three reads are plumbing rather than a fork, and stay silent:

  • seeding a ref with the flag — useRef(inCanvas);
  • keeping that ref current — inCanvasRef.current = inCanvas;
  • naming it as a hook dependency — }, [inCanvas]).

A fork reached through such a ref (if (inCanvasRef.current) return; — what an imperative event handler needs, since it reads outside render) is followed one level and reports at the decision itself.

The contract is that a live island runs its published behavior in the canvas. inCanvas is the one sanctioned way to break it, and it is deliberately plain React — an ordinary boolean, no wrapper, no declaration — so the rare case that needs it costs nothing to write.

That cheapness is also the risk. With nothing asking why, inCanvas slides from “this one takeover cannot run here” into a second, quietly divergent editor rendering of the island: an editor mode. Authors then debug two behaviors instead of one, and the author sees something in the canvas that no visitor will ever see.

Naming the protected behavior is the check. A fork you can justify in one line — a fullscreen modal, a scroll lock, a takeover that hides the page — is the escape hatch working. A fork you cannot is a sign you wanted editor = { live: false } for the whole island, or wanted nothing at all.

The marker is greppable on purpose: editor-reason: is how every hatch in a workspace is found at once, the same way css-reason: opens every hand-written stylesheet.

Write the reason, or remove the fork.

"use client";
import { type IslandEditor, useEditor } from "@homepages/template-kit";
import { useState } from "react";
export const editor = { live: true } satisfies IslandEditor;
export default function Slider() {
const [galleryOpen, setGalleryOpen] = useState(false);
const { inCanvas } = useEditor();
const openGallery = () => {
if (inCanvas) return;
setGalleryOpen(true);
};
return (
<div>
{!inCanvas && <button onClick={openGallery}>Expand</button>}
{galleryOpen && <div role="dialog"></div>}
</div>
);
}
"use client";
import { type IslandEditor, useEditor } from "@homepages/template-kit";
import { useState } from "react";
export const editor = { live: true } satisfies IslandEditor;
export default function Slider() {
const [galleryOpen, setGalleryOpen] = useState(false);
const { inCanvas } = useEditor();
const openGallery = () => {
// editor-reason: the gallery is a fullscreen scroll-locking takeover — it would
// cover the whole page the author is editing.
if (inCanvas) return;
setGalleryOpen(true);
};
return (
<div>
{/* editor-reason: don't advertise a takeover openGallery refuses to perform. */}
{!inCanvas && <button onClick={openGallery}>Expand</button>}
{galleryOpen && <div role="dialog"></div>}
</div>
);
}
  • Islands — when the hatch is the right answer, and when live: false is.
  • require-island-editor — the live declaration itself.
  • no-editor-globals — the other half: useEditor() is the only way to learn you are in the canvas.