facebook/lexical · error

Invalid state key: ${key}

Error message

Invalid state key: ${key}

What it means

SyncV2 stores Lexical node state (node.__state) in Yjs element attributes under keys prefixed with 's_'. attrKeyToStateKey converts an attribute key back to a state key and throws if the attribute does not carry the required prefix. This guards against reading foreign attributes (user data, other libs) as editor state.

Source

Thrown at packages/lexical-yjs/src/SyncV2.ts:970

const propertiesToAttributes = (node: LexicalNode, meta: BindingV2) => {
  const defaultProperties = getDefaultNodeProperties(node, meta);
  const attrs: Record<string, unknown> = {};
  Object.entries(defaultProperties).forEach(([property, defaultValue]) => {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const value = (node as any)[property];
    if (value !== defaultValue) {
      attrs[property] = value;
    }
  });
  return attrs;
};

const STATE_KEY_PREFIX = 's_';
const stateKeyToAttrKey = (key: string): `s_${string}` => `s_${key}`;
const attrKeyToStateKey = (key: string) => {
  if (!key.startsWith(STATE_KEY_PREFIX)) {
    throw new Error(`Invalid state key: ${key}`);
  }
  return key.slice(STATE_KEY_PREFIX.length);
};

const stateToAttributes = (node: LexicalNode) => {
  const state = node.__state;
  if (!state) {
    return {};
  }
  const [unknown = {}, known] = state.getInternalState();
  const attrs: Record<string, unknown> = {};
  for (const [k, v] of Object.entries(unknown)) {
    attrs[stateKeyToAttrKey(k)] = v;
  }
  for (const [stateConfig, v] of known) {
    attrs[stateKeyToAttrKey(stateConfig.key)] = stateConfig.unparse(v);
  }
  return attrs;

View on GitHub (pinned to 76a22dcba9)

Solutions

  1. Ensure all attributes written to shared Yjs elements that represent node state use the stateKeyToAttrKey helper (adds the 's_' prefix).
  2. Filter the attribute map before conversion: only pass keys matching /^s_/ to attrKeyToStateKey.
  3. Check for version mismatch between the code writing attributes and the code reading them; upgrade both clients.
  4. If migrating old documents, run a one-time migration to rename attributes to the prefixed form.

Example fix

// before
const stateKey = attrKeyToStateKey(attrName);
// after
if (attrName.startsWith('s_')) {
  const stateKey = attrKeyToStateKey(attrName);
}
Defensive patterns

Strategy: validation

Validate before calling

function isStateAttr(key: string): boolean {
  return typeof key === 'string' && key.startsWith('s_');
}
Object.keys(attrs).filter(isStateAttr).map(attrKeyToStateKey);

Type guard

function isStateAttrKey(key: string): key is `s_${string}` {
  return key.startsWith('s_');
}

Try / catch

try {
  stateKey = attrKeyToStateKey(key);
} catch (e) {
  if (e instanceof Error && e.message.startsWith('Invalid state key')) {
    console.warn('Skipping non-state attribute', key);
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: attrKeyToStateKey is called on an attribute key from a Yjs XmlElement that does not start with 's_' — i.e. stateToAttributes/attribute iteration encounters an attribute written outside the state-key convention.

Common situations: Hand-edited or migrated Yjs documents with attributes lacking the 's_' prefix; a custom sync integration writing raw attribute names; version skew where the prefix scheme changed between library versions.

Related errors


AI-assisted analysis of facebook/lexical@76a22dcba9 (2026-08-31). Data as JSON: /api/errors/509c847280bd7625. Report an issue: GitHub.