docs/solutions/developer-experience/2026-04-08-slate-v2-shouldnormalize-must-be-pass-level-and-fallback-safe.md
A partially-wired shouldNormalize hook looked fine on the narrow editor seam,
but it widened core assumptions too early. The result was an incoherent hook
contract and breakage in broader Slate test families.
yarn test:custom failed across range-ref coverage with
TypeError: editor.shouldNormalize is not a functionshouldNormalize firing twice with the
same { iteration, operation } payload during one passcreateEditor() and the editor types without a core
fallback. Wrapped or older editor-shaped instances then crashed as soon as
normalization ran.shouldNormalize inside the per-entry loop. That gave the same
options to multiple calls and made the API shape vague.Keep shouldNormalize as a narrow gate over the custom normalization pass, and
evaluate it once per pass with a safe fallback:
const normalizeOptions: NormalizeNodeOptions = {
operation: transaction.operations[transaction.operations.length - 1],
};
const shouldNormalize =
(editor as Partial<Editor>).shouldNormalize?.({
iteration,
operation: normalizeOptions.operation,
}) ?? true;
if (!shouldNormalize) {
return;
}
for (const entry of entries) {
const beforeMutationCount = transaction.mutationCount;
editor.normalizeNode(entry, normalizeOptions);
if (transaction.mutationCount !== beforeMutationCount) {
changed = true;
break;
}
}
Then prove the contract in snapshot-contract.ts:
createEditor() exposes shouldNormalizeEditor.shouldNormalize(...) delegates through the instance seamshouldNormalize runs once per custom normalization passfalse skips the custom pass for that transactionshouldNormalize now has one job: decide whether the current custom
normalization pass should run. It no longer masquerades as an entry-level hook,
and core no longer explodes when an older editor instance has not grown the new
method yet.