Understanding Notion’s Block‑Based Editor Architecture and Operations
The article explains how Notion implements a block‑tree data model and a controlled contenteditable editor using React, detailing the data layer, operation (op) system, rendering components, selection handling, text styling, and copy‑paste mechanisms, all backed by transactional undo/redo logic.
Notion is presented as a powerful knowledge‑management tool, and the article dives into its technical underpinnings, focusing on how the product builds a rich‑text editor on top of a block‑tree data model.
Data layer : Every piece of content in Notion is a block with fields such as properties , parent_id , type , and version . Blocks form a hierarchical block‑tree that mirrors the DOM tree, enabling flexible composition of pages, tables, images, and other elements.
Operations (op) and transactions : Modifications to the block‑tree are expressed as atomic op objects (e.g., set , update , listBefore , listAfter , listRemove ). A series of ops are bundled into a Transaction , which is pushed onto a revisionStack to provide undo/redo functionality. The stack records both the forward ops and their pre‑computed inverse ops.
Rendering layer : React renders each block via a wrapper component that handles generic behaviours (drag‑and‑drop, selection halo, event forwarding). The core rendering code looks like:
renderComponent(){
const {isDragging, initiator, hasDragged, shouldShowDropParentHalo, shouldShowSelectionHalo, currentDropZone, dropZoneHint, dropZoneHintIndex, shouldRenderSelectionHalo, shouldRenderDropParentHalo, shouldRenderDropZoneHint, shouldRenderDropZone, propStoreType} = this.computedStore.state;
const m = c || d || u || p;
const g = "".concat(x.sb, " notion-").concat(h, "-block");
return this.renderDraggable(h => React.createElement("div", Object.assign({
style: this.getStyle({isPositionRelative: m, isDragging: e, hasDragged: n, initiator: t}),
"data-block-id": this.props.store.id,
className: this.props.className ? `${g} ${this.props.className}` : g
}, on(this.props.store.id, {onDoubleClick: this.handleDoubleClick, onContextMenu: this.handleContextMenu, onMouseOver: this.handleMouseOver, onMouseOut: this.handleMouseOut, onClick: this.handleClick, onMouseMove: this.props.onMouseMove, onMouseDown: this.props.onMouseDown, onMouseEnter: this.props.onMouseEnter, onMouseLeave: this.props.onMouseLeave})), this.props.children, c && this.renderSelectionHalo(i), d && this.renderDropParentHalo(o), u && this.renderDropZoneHint(a, l), p && this.renderCurrentDropZone(s))
}The component caches its DOM position in selectableRectMap to enable fast block‑selection based on mouse coordinates.
Selection handling : Notion supports two kinds of selection – block selection (multiple sibling blocks) and text selection inside contenteditable blocks. Block selection works by checking the cached rectangles against the mouse pointer, while text selection relies on the browser’s Selection API and maps anchorNode / focusNode to stored token indices.
Text storage and styling : Styled text is stored as an array of intervals, each interval containing the raw characters and a list of style tokens (e.g., b for bold, h for color). When a user applies a style, the editor performs three steps – interval lookup, interval modification, and interval recombination – to keep the data compact.
function Ee(e){
const {environment:t, store:n, selection:r, annotation:a, transaction:s} = e, l = n.getValue();
if(l){
const e = a, c = i.F(e), {tokensInsideRange:d} = i.Nb(l, r.startIndex, r.endIndex);
let u;
u = o.a.every(d, e => i.db(e).some(e => i.F(e) === c))
? d.map(e => { const t = i.fb(e), n = i.db(e).filter(e => i.F(e) !== c); return i.n(t, n) })
: d.map(t => { const n = i.fb(t), r = i.db(t).filter(e => i.F(e) !== c); r.push(e); return i.n(n, r) });
const p = n.getValue(), h = i.p(p, r.startIndex, r.endIndex);
Me({environment:t, store:n, value:i.nb(h, u, r.startIndex), transaction:s})
}
}Copy & paste : For internal copies, Notion serialises selected blocks (including their sub‑tree) to a custom clipboard MIME type text/_notion-blocks-v2-production . For external sources, the editor first converts blocks to Markdown using a set of per‑type renderers, then runs the Markdown through markdown‑it to produce HTML, which is placed on the clipboard as text/html . The reverse process parses incoming HTML or plain text, creates corresponding ops, and inserts the new blocks.
function $t(e, t){
if(!e.clipboardData) return;
const n = t.map(e => Wt({}, e, {blockSubtree: e.blockSubtree ? e.blockSubtree.toJson() : void 0}));
e.clipboardData.setData(Jt, JSON.stringify(n))
}Overall, Notion’s architecture cleanly separates the data model (block‑tree) from the presentation layer (React rendering), uses a transactional operation system for all mutations, and provides extensible mechanisms for selection, styling, and clipboard integration, making it easy to add new block types and features.
Sohu Tech Products
A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.