toeverything/AFFiNE · error · BlockSuiteError
ReactiveProxyError
ReactiveProxyError
Error message
flatY2Native does not support Y.Map as value of Y.Map
What it means
In the flat reactive layer (ReactiveFlatYMap), props are stored as flattened dot-notation keys (prop:a.b) and initializeData only knows how to materialize Boxed values, Y.Array, Y.Text and plain scalars. A Y.Map nested as a prop value has no flat representation, so hydration throws ReactiveProxyError instead of producing a broken proxy.
Solutions
- Model nested data as flattened dot-notation keys (as addBlock does for isFlatData blocks via the recursive run() at crud.ts:100-110), not nested Y.Maps
- Use supported containers for structured values: Y.Array (arrays), Y.Text (rich text), Boxed (opaque values)
- Do not point the same flavour at both data models — pick tree or flat and keep all writers consistent
- Migrate existing props: walk the Y.Map and flatten each entry into prop:parent.child keys
Example fix
// before — nested Y.Map under a flat-model prop
yBlock.set('prop:style', new Y.Map([['color', 'red']]));
// hydration -> flatY2Native does not support Y.Map as value of Y.Map
// after — flatten to dot-notation keys
yBlock.set('prop:style.color', 'red');
// or model it as a supported type
yBlock.set('prop:tags', new Y.Array(['a', 'b'])); Defensive patterns
Strategy: type-guard
Validate before calling
import * as Y from 'yjs';
function isFlatSafeValue(value: unknown): boolean {
return (
value === null ||
['string', 'number', 'boolean'].includes(typeof value) ||
value instanceof Y.Array ||
value instanceof Y.Text
);
}
// before writing a prop on a flat-model block:
if (!isFlatSafeValue(value)) {
throw new Error('flatten the value into dot-notation keys instead');
} Type guard
const isFlatSafeValue = (v: unknown): boolean => v === null || ['string', 'number', 'boolean'].includes(typeof v) || v instanceof Y.Array || v instanceof Y.Text;
Try / catch
try {
doc.load();
} catch (e) {
if (e instanceof BlockSuiteError && e.code === ErrorCode.ReactiveProxyError) {
// a prop holds a Y.Map: flatten it into prop:a.b keys, then reload
}
} Prevention
- Choose one data model per flavour and never mix writers
- Store nested objects as flattened dot-notation keys, arrays as Y.Array, rich text as Y.Text, opaque values as Boxed
- Lint raw yBlock.set('prop:...', new Y.Map(...)) calls in reviews
When it happens
Trigger: A flat-data-model block whose prop:* entry holds a Y.Map — typically data written by the tree-model path (native2Y turns nested objects into Y.Map, crud.ts:115) and then read through the flat layer, or raw Yjs writes putting a Y.Map under a prop key.
Common situations: Switching a block family from the tree data model to isFlatData while old docs still hold nested Y.Map props; import scripts reusing tree-model serialization for flat blocks; two features writing the same flavour through different data models.
Related errors
- Invalid key for
- props is only supported in flat data model
- ErrorCode.ModelCRUDError
- ModelCRUDError
- ReactiveProxyError
AI-assisted analysis of toeverything/AFFiNE@b4c8548c09 (2026-08-18).
Data as JSON: /api/errors/96a1011cb5a8c8cc.
Report an issue: GitHub.
Appendix: source
Thrown at blocksuite/framework/store/src/reactive/flat-native-y/initialize.ts:37
}: InitializeDataOptions): UnRecord => {
const root: UnRecord = {};
Array.from(yMap.entries()).forEach(([key, value]) => {
if (key.startsWith('sys')) {
return;
}
const keys = keyWithoutPrefix(key).split('.');
const firstKey = keys[0];
let finalData = value;
if (Boxed.is(value)) {
finalData = transform(firstKey, new Boxed(value), value);
} else if (value instanceof YArray) {
finalData = transform(firstKey, value.toArray(), value);
} else if (value instanceof YText) {
const next = new Text(value);
finalData = transform(firstKey, next, value);
} else if (value instanceof YMap) {
throw new BlockSuiteError(
BlockSuiteError.ErrorCode.ReactiveProxyError,
'flatY2Native does not support Y.Map as value of Y.Map'
);
} else {
finalData = transform(firstKey, value, value);
}
const allLength = keys.length;
void keys.reduce((acc: UnRecord, key, index) => {
if (!acc[key] && index !== allLength - 1) {
const path = keys.slice(0, index + 1).join('.');
const data = getProxy({} as UnRecord, root, path);
acc[key] = data;
}
if (index === allLength - 1) {
acc[key] = finalData;
}
return acc[key] as UnRecord;
}, root);View on GitHub (pinned to b4c8548c09)