You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(react): stabilize user/users/modules props to prevent re-init flicker (SD-2635) (#2876)
* fix(react): stabilize user/users/modules props to prevent re-init flicker (SD-2635)
Consumers passing inline object literals (the idiomatic React pattern) caused
a full SuperDoc destroy + rebuild on every parent re-render because the
main useEffect compared these props by reference identity. When a consumer
stored the SuperDoc instance in state from onReady, the resulting re-render
supplied fresh references and triggered another cycle — observed as 4 full
destroy/re-init cycles in ~100ms with visible display:none/block flicker.
Wrap user, users, and modules in a new useStableValue hook that returns a
reference-stable version only changing identity when the structural content
actually changes (via JSON.stringify compare, run only on reference-change).
Semantics are strictly a superset of the prior behavior — value changes still
rebuild; reference-only changes no longer do.
* fix(react): drop modules from structural memo; harden tests (SD-2635 review round 2)
Review feedback addressed:
- Correctness: `modules` carries function-valued options (`permissionResolver`,
`popoverResolver`) and live objects (`collaboration.ydoc`/`provider`) that
JSON.stringify silently drops or collapses. Keeping it on structural
compare would silently ignore real config changes. Reverted to reference
identity for `modules`; `user` and `users` stay memoized since they are
plain data.
- Naming: renamed `useStableValue` → `useStructuralMemo` to match the
useMemo family and signal non-referential equality.
- JSDoc: expanded the hook's docblock to spell out every JSON.stringify
footgun consumers need to know about (functions dropped, class instances
collapsed, undefined values dropped, NaN/Infinity → null, circular refs,
key-insertion-order sensitivity).
- Tests: replaced the brittle `setTimeout(100)` negative assertion with a
synchronous `ref.getInstance()` identity check; strengthened the "rebuilds
on change" test to also assert a second onReady + a fresh instance;
added a `users`-prop stability test; added a StrictMode + rerender test
to guard the ref-write-during-render path.
* fix(react): define event types explicitly to unblock CI type-check (SD-2635)
CI type-check failed with `Property 'onEditorUpdate' does not exist on
type 'Config'` even though the JSDoc `Config` typedef in superdoc clearly
declares it. The old approach derived SuperDocEditorUpdateEvent and
SuperDocTransactionEvent via
`Parameters<NonNullable<SuperDocConstructorConfig['onEditorUpdate']>>[0]`,
which walked a chain:
ConstructorParameters<typeof SuperDoc>[0]
→ @param {Config} in core/SuperDoc.js
→ @typedef Config in core/types/index.js
→ @Property onEditorUpdate with @typedef EditorUpdateEvent
This chain resolves fine locally but breaks on CI — the exact failure
point in JSDoc resolution depends on TS version, moduleResolution mode,
and the `customConditions: ["source"]` in tsconfig.base.json (which
routes imports to the raw .js source instead of the built .d.ts).
Define the two event shapes inline instead, mirroring superdoc's JSDoc.
No behavior change for consumers — same fields, same optionality.
* fix(react): round-4 review feedback — lock modules contract, restore transaction: any (SD-2635)
Addressing consensus findings from the round-3 review:
- Revert `transaction: unknown` back to `any` to match superdoc's upstream
typedef. `unknown` was a narrowing from `any` that would have broken
existing consumer code like `event.transaction.docChanged`.
- Re-export `EditorSurface` from the package barrel. It's referenced by
two public event interfaces but wasn't exported, so consumers couldn't
name the `surface` field's type.
- Symmetrize per-field JSDoc on `SuperDocTransactionEvent` to match its
sibling `SuperDocEditorUpdateEvent`.
- Add a regression test asserting that passing a new `modules` object
with identical content DOES rebuild the editor. This pins the contract
that `modules` stays on reference identity (it can carry functions and
live objects that structural compare misses) — a future "cleanup" that
wraps `modules` in useStructuralMemo would silently re-introduce the
SD-2635 blocker without this test.
Also trim commentary added during rounds 1-3: the stabilization rationale
was documented twice in SuperDocEditor.tsx (once at the destructure,
once near the dep array), and the types.ts docstrings leaked maintainer
build-tooling rationale into consumer IDE hovers.
* refactor(react): swap JSON.stringify compare for lodash.isequal; drop TransactionEvent (SD-2635)
- Swap the hand-rolled JSON.stringify-based structural compare for
`lodash.isequal`. +10KB raw / +3.7KB gzipped cost, but removes the
5-bullet footgun list from JSDoc and gets proper handling of Dates,
Maps, circular refs, and key ordering for free. For our use (stabilizing
`user`/`users` plain-data records) the 3.7KB buys nothing practical,
but it removes ~40 lines of hand-maintained code and a bag of edge
cases. Trade maintenance cost for bundle cost.
- Rename `useStructuralMemo` → `useMemoByValue`. Plainer, matches the
useMemo family, says what it does without jargon.
- Drop `SuperDocTransactionEvent` from the public API. It was exported
but never wired up to a callback prop in `CallbackProps`, so nothing
fires it. Its shape also leaked ProseMirror's `transaction` object —
a deprecated surface per superdoc's own notes. Removing it now is
cheaper than removing it after someone starts relying on it.
- Replace the JSON-stringify-specific unit tests with tests that exercise
what lodash.isequal actually gives us (key-order insensitivity,
same-reference function equality). 27/27 tests pass.
* refactor(react): drop lodash.isequal dep, inline JSON.stringify compare (SD-2635)
Round 5 added lodash.isequal for `useMemoByValue` correctness at the
cost of +10KB raw / +3.7KB gzipped. For the two props actually using the
hook (`user` and `users` — small plain-data records) the extra
correctness buys nothing practical: those records contain strings only,
no Dates, no Maps, no circular refs, no functions.
Revert to an inline JSON.stringify compare wrapped in a `try/catch` for
circular refs. The hook is now ~15 lines, zero dependencies, and the
unit test that required lodash (key-order insensitivity) is replaced by
a circular-ref fallback test that matches the implementation.
Bundle is back to 3.69 KB / 1.60 KB gzipped.
* test(react): cover StrictMode + user prop value change (SD-2635)
The existing StrictMode test only proved same-content rerender stays
stable — the positive path (real value change under StrictMode still
triggers destroy + fresh onReady) wasn't exercised. Coverage audit
after rounds 2-6 flagged this as the one gap worth closing before
ship. Mirrors the existing non-StrictMode rebuild test, wrapped in
<StrictMode>.
0 commit comments