@atlaskit/editor-plugin-synced-block 8.2.10 → 8.2.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # @atlaskit/editor-plugin-synced-block
2
2
 
3
+ ## 8.2.11
4
+
5
+ ### Patch Changes
6
+
7
+ - [`a160344820ea5`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/a160344820ea5) -
8
+ EDITOR-6929: Fix React re-render cascade by (1) returning same plugin state reference from apply()
9
+ when nothing changed, (2) memoizing getSharedState to return a stable reference, and (3) guarding
10
+ contentComponent to skip rendering when hasSyncedBlocks is false. All gated behind
11
+ editor_synced_block_perf experiment.
12
+ - Updated dependencies
13
+
3
14
  ## 8.2.10
4
15
 
5
16
  ### Patch Changes
@@ -357,7 +357,7 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
357
357
  // When the perf gate is ON and the doc has synced blocks we do a
358
358
  // single traversal here; afterwards `apply()` will map or rebuild
359
359
  // only when a status signal changes.
360
- var initStatusDecorationSet = (0, _expValEquals.expValEquals)('editor_synced_block_perf', 'isEnabled', true) && docHasSyncedBlocks ? buildStatusDecorations(instance.doc, syncBlockStore, initIsOffline, initIsViewMode, initIsDragging) : _view.DecorationSet.empty;
360
+ var initStatusDecorationSet = docHasSyncedBlocks && (0, _expValEquals.expValEquals)('editor_synced_block_perf', 'isEnabled', true) ? buildStatusDecorations(instance.doc, syncBlockStore, initIsOffline, initIsViewMode, initIsDragging) : _view.DecorationSet.empty;
361
361
  return {
362
362
  selectionDecorationSet: (0, _selectionDecorations.calculateDecorations)(instance.doc, instance.selection, instance.schema),
363
363
  activeFlag: false,
@@ -374,6 +374,7 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
374
374
  apply: function apply(tr, currentPluginState, oldEditorState) {
375
375
  var _meta$activeFlag, _meta$bodiedSyncBlock;
376
376
  var meta = tr.getMeta(syncedBlockPluginKey);
377
+ var isPerfExperimentOn = (0, _expValEquals.expValEquals)('editor_synced_block_perf', 'isEnabled', true);
377
378
  var activeFlag = currentPluginState.activeFlag,
378
379
  selectionDecorationSet = currentPluginState.selectionDecorationSet,
379
380
  bodiedSyncBlockDeletionStatus = currentPluginState.bodiedSyncBlockDeletionStatus,
@@ -387,11 +388,19 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
387
388
  // Lazy-init bookkeeping: once a synced block enters the document we
388
389
  // flip `hasSyncedBlocks` to `true` for the lifetime of this editor
389
390
  var nextHasSyncedBlocks = prevHasSyncedBlocks;
390
- if (!prevHasSyncedBlocks && tr.docChanged && (0, _expValEquals.expValEquals)('editor_synced_block_perf', 'isEnabled', true)) {
391
+ if (!prevHasSyncedBlocks && tr.docChanged && isPerfExperimentOn) {
391
392
  if ((0, _transactionInsertsSyncedBlock.transactionInsertsSyncedBlock)(tr)) {
392
393
  nextHasSyncedBlocks = true;
393
394
  }
394
395
  }
396
+
397
+ // --- Fast path (EDITOR-6929): when `hasSyncedBlocks` is false,
398
+ // no meta is set, and the selection/doc haven't changed in a way
399
+ // that affects our state, return the SAME object reference so
400
+ // SharedStateAPI skips notifying subscribers. ---
401
+ if (!nextHasSyncedBlocks && !meta && !tr.docChanged && tr.selection.eq(oldEditorState.selection) && isPerfExperimentOn) {
402
+ return currentPluginState;
403
+ }
395
404
  var newDecorationSet = tr.docChanged ? selectionDecorationSet.map(tr.mapping, tr.doc) // only map if document changed
396
405
  : selectionDecorationSet;
397
406
  if (!tr.selection.eq(oldEditorState.selection)) {
@@ -415,7 +424,7 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
415
424
  var nextIsOffline = prevOffline;
416
425
  var nextIsViewMode = prevViewMode;
417
426
  var nextIsDragging = prevDragging;
418
- if ((0, _expValEquals.expValEquals)('editor_synced_block_perf', 'isEnabled', true)) {
427
+ if (isPerfExperimentOn) {
419
428
  if (!nextHasSyncedBlocks) {
420
429
  // No synced blocks → keep empty status decorations
421
430
  nextStatusDecorationSet = _view.DecorationSet.empty;
@@ -444,14 +453,24 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
444
453
  }
445
454
  var newPosEntry = meta === null || meta === void 0 ? void 0 : meta.retryCreationPos;
446
455
  var newRetryCreationPosMap = mapRetryCreationPosMap(retryCreationPosMap, newPosEntry, tr.mapping.map.bind(tr.mapping));
456
+ var nextActiveFlag = (_meta$activeFlag = meta === null || meta === void 0 ? void 0 : meta.activeFlag) !== null && _meta$activeFlag !== void 0 ? _meta$activeFlag : activeFlag;
457
+ var nextBodiedSyncBlockDeletionStatus = (_meta$bodiedSyncBlock = meta === null || meta === void 0 ? void 0 : meta.bodiedSyncBlockDeletionStatus) !== null && _meta$bodiedSyncBlock !== void 0 ? _meta$bodiedSyncBlock : bodiedSyncBlockDeletionStatus;
458
+ var nextHasUnsavedBodiedSyncBlockChanges = syncBlockStore.sourceManager.hasUnsavedChanges();
459
+
460
+ // --- Reference equality (EDITOR-6929): return the same object
461
+ // when ALL fields are reference-equal to avoid SharedStateAPI
462
+ // notifying subscribers and triggering React re-renders. ---
463
+ if (nextActiveFlag === activeFlag && newDecorationSet === selectionDecorationSet && newRetryCreationPosMap === retryCreationPosMap && nextHasSyncedBlocks === prevHasSyncedBlocks && nextBodiedSyncBlockDeletionStatus === bodiedSyncBlockDeletionStatus && nextHasUnsavedBodiedSyncBlockChanges === currentPluginState.hasUnsavedBodiedSyncBlockChanges && nextStatusDecorationSet === prevStatusDecorationSet && nextIsOffline === prevOffline && nextIsViewMode === prevViewMode && nextIsDragging === prevDragging && isPerfExperimentOn) {
464
+ return currentPluginState;
465
+ }
447
466
  return {
448
- activeFlag: (_meta$activeFlag = meta === null || meta === void 0 ? void 0 : meta.activeFlag) !== null && _meta$activeFlag !== void 0 ? _meta$activeFlag : activeFlag,
467
+ activeFlag: nextActiveFlag,
449
468
  selectionDecorationSet: newDecorationSet,
450
469
  syncBlockStore: syncBlockStore,
451
470
  retryCreationPosMap: newRetryCreationPosMap,
452
471
  hasSyncedBlocks: nextHasSyncedBlocks,
453
- bodiedSyncBlockDeletionStatus: (_meta$bodiedSyncBlock = meta === null || meta === void 0 ? void 0 : meta.bodiedSyncBlockDeletionStatus) !== null && _meta$bodiedSyncBlock !== void 0 ? _meta$bodiedSyncBlock : bodiedSyncBlockDeletionStatus,
454
- hasUnsavedBodiedSyncBlockChanges: syncBlockStore.sourceManager.hasUnsavedChanges(),
472
+ bodiedSyncBlockDeletionStatus: nextBodiedSyncBlockDeletionStatus,
473
+ hasUnsavedBodiedSyncBlockChanges: nextHasUnsavedBodiedSyncBlockChanges,
455
474
  statusDecorationSet: nextStatusDecorationSet,
456
475
  prevIsOffline: nextIsOffline,
457
476
  prevIsViewMode: nextIsViewMode,
@@ -7,6 +7,7 @@ Object.defineProperty(exports, "__esModule", {
7
7
  exports.syncedBlockPlugin = void 0;
8
8
  var _react = _interopRequireDefault(require("react"));
9
9
  var _adfSchema = require("@atlaskit/adf-schema");
10
+ var _hooks = require("@atlaskit/editor-common/hooks");
10
11
  var _editorSyncedBlockProvider = require("@atlaskit/editor-synced-block-provider");
11
12
  var _expValEquals = require("@atlaskit/tmp-editor-statsig/exp-val-equals");
12
13
  var _experiments = require("@atlaskit/tmp-editor-statsig/experiments");
@@ -22,15 +23,50 @@ var _floatingToolbar = require("./ui/floating-toolbar");
22
23
  var _quickInsert = require("./ui/quick-insert");
23
24
  var _SyncBlockRefresher = require("./ui/SyncBlockRefresher");
24
25
  var _toolbarComponents = require("./ui/toolbar-components");
25
- var syncedBlockPlugin = exports.syncedBlockPlugin = function syncedBlockPlugin(_ref) {
26
- var _api$editorViewMode, _api$analytics, _api$blockMenu, _config$enableSourceC, _api$toolbar, _config$enableSourceC2;
27
- var config = _ref.config,
26
+ /**
27
+ * EDITOR-6929 / PR-G: Guard contentComponent rendering.
28
+ * When `hasSyncedBlocks` is false return null
29
+ * to avoid mounting SyncBlockRefresher, DeleteConfirmationModal, and Flag —
30
+ * their hooks (useSharedPluginStateWithSelector) would execute selectors on
31
+ * every transaction for no benefit on the ~99.98% of pages with zero synced
32
+ * blocks.
33
+ */
34
+ var LazySyncedBlockUI = function LazySyncedBlockUI(_ref) {
35
+ var syncBlockStoreManager = _ref.syncBlockStore,
28
36
  api = _ref.api;
37
+ var hasSyncBlocks = (0, _hooks.useSharedPluginStateWithSelector)(api, ['syncedBlock'], function (states) {
38
+ var _states$syncedBlockSt;
39
+ return (_states$syncedBlockSt = states.syncedBlockState) === null || _states$syncedBlockSt === void 0 ? void 0 : _states$syncedBlockSt.hasSyncedBlocks;
40
+ });
41
+ if (!hasSyncBlocks && (0, _expValEquals.expValEquals)('editor_synced_block_perf', 'isEnabled', true)) {
42
+ return null;
43
+ }
44
+ return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(_SyncBlockRefresher.SyncBlockRefresher, {
45
+ syncBlockStoreManager: syncBlockStoreManager,
46
+ api: api
47
+ }), /*#__PURE__*/_react.default.createElement(_DeleteConfirmationModal.DeleteConfirmationModal, {
48
+ syncBlockStoreManager: syncBlockStoreManager,
49
+ api: api
50
+ }), /*#__PURE__*/_react.default.createElement(_Flag.Flag, {
51
+ api: api
52
+ }));
53
+ };
54
+ var syncedBlockPlugin = exports.syncedBlockPlugin = function syncedBlockPlugin(_ref2) {
55
+ var _api$editorViewMode, _api$analytics, _api$blockMenu, _config$enableSourceC, _api$toolbar, _config$enableSourceC2;
56
+ var config = _ref2.config,
57
+ api = _ref2.api;
29
58
  var refs = {};
30
59
  var viewMode = api === null || api === void 0 || (_api$editorViewMode = api.editorViewMode) === null || _api$editorViewMode === void 0 || (_api$editorViewMode = _api$editorViewMode.sharedState.currentState()) === null || _api$editorViewMode === void 0 ? void 0 : _api$editorViewMode.mode;
31
60
  var syncBlockStore = new _editorSyncedBlockProvider.SyncBlockStoreManager(config === null || config === void 0 ? void 0 : config.syncBlockDataProvider, viewMode, config === null || config === void 0 ? void 0 : config.__livePage);
32
61
  var isPerfExperimentOn = (0, _expValEquals.expValEquals)('editor_synced_block_perf', 'isEnabled', true);
33
62
  syncBlockStore.setFireAnalyticsEvent(api === null || api === void 0 || (_api$analytics = api.analytics) === null || _api$analytics === void 0 || (_api$analytics = _api$analytics.actions) === null || _api$analytics === void 0 ? void 0 : _api$analytics.fireAnalyticsEvent);
63
+
64
+ // --- Memoized getSharedState (EDITOR-6929 / PR-F) ---
65
+ // Cache the last returned shared state object. On each call, perform a
66
+ // shallow comparison of all fields against the cached value. If nothing
67
+ // changed, return the cached reference so SharedStateAPI subscribers
68
+ // (React components) skip re-rendering.
69
+ var cachedSharedState;
34
70
  api === null || api === void 0 || (_api$blockMenu = api.blockMenu) === null || _api$blockMenu === void 0 || _api$blockMenu.actions.registerBlockMenuComponents((0, _blockMenuComponents.getBlockMenuComponents)(api, (_config$enableSourceC = config === null || config === void 0 ? void 0 : config.enableSourceCreation) !== null && _config$enableSourceC !== void 0 ? _config$enableSourceC : false));
35
71
  api === null || api === void 0 || (_api$toolbar = api.toolbar) === null || _api$toolbar === void 0 || _api$toolbar.actions.registerComponents((0, _toolbarComponents.getToolbarComponents)(api, (_config$enableSourceC2 = config === null || config === void 0 ? void 0 : config.enableSourceCreation) !== null && _config$enableSourceC2 !== void 0 ? _config$enableSourceC2 : false));
36
72
  return {
@@ -72,9 +108,9 @@ var syncedBlockPlugin = exports.syncedBlockPlugin = function syncedBlockPlugin(_
72
108
  return (0, _editorCommands.copySyncedBlockReferenceToClipboardEditorCommand)(syncBlockStore, inputMethod, api);
73
109
  },
74
110
  insertSyncedBlock: function insertSyncedBlock() {
75
- return function (_ref2) {
111
+ return function (_ref3) {
76
112
  var _api$analytics3;
77
- var tr = _ref2.tr;
113
+ var tr = _ref3.tr;
78
114
  if (!(config !== null && config !== void 0 && config.enableSourceCreation)) {
79
115
  return null;
80
116
  }
@@ -113,35 +149,36 @@ var syncedBlockPlugin = exports.syncedBlockPlugin = function syncedBlockPlugin(_
113
149
  return (0, _floatingToolbar.getToolbarConfig)(state, intl, api, syncBlockStore);
114
150
  }
115
151
  },
116
- contentComponent: function contentComponent(_ref3) {
117
- var containerElement = _ref3.containerElement,
118
- wrapperElement = _ref3.wrapperElement,
119
- popupsMountPoint = _ref3.popupsMountPoint;
152
+ contentComponent: function contentComponent(_ref4) {
153
+ var containerElement = _ref4.containerElement,
154
+ wrapperElement = _ref4.wrapperElement,
155
+ popupsMountPoint = _ref4.popupsMountPoint;
120
156
  refs.containerElement = containerElement || undefined;
121
157
  refs.popupsMountPoint = popupsMountPoint || undefined;
122
158
  refs.wrapperElement = wrapperElement || undefined;
123
- return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(_SyncBlockRefresher.SyncBlockRefresher, {
124
- syncBlockStoreManager: syncBlockStore,
125
- api: api
126
- }), /*#__PURE__*/_react.default.createElement(_DeleteConfirmationModal.DeleteConfirmationModal, {
127
- syncBlockStoreManager: syncBlockStore,
159
+ return /*#__PURE__*/_react.default.createElement(LazySyncedBlockUI, {
160
+ syncBlockStore: syncBlockStore,
128
161
  api: api
129
- }), /*#__PURE__*/_react.default.createElement(_Flag.Flag, {
130
- api: api
131
- }));
162
+ });
132
163
  },
133
164
  getSharedState: function getSharedState(editorState) {
134
165
  if (!editorState) {
135
166
  return;
136
167
  }
137
- var _syncedBlockPluginKey2 = _main.syncedBlockPluginKey.getState(editorState),
138
- activeFlag = _syncedBlockPluginKey2.activeFlag,
139
- currentSyncBlockStore = _syncedBlockPluginKey2.syncBlockStore,
140
- bodiedSyncBlockDeletionStatus = _syncedBlockPluginKey2.bodiedSyncBlockDeletionStatus,
141
- retryCreationPosMap = _syncedBlockPluginKey2.retryCreationPosMap,
142
- hasSyncedBlocks = _syncedBlockPluginKey2.hasSyncedBlocks,
143
- hasUnsavedBodiedSyncBlockChanges = _syncedBlockPluginKey2.hasUnsavedBodiedSyncBlockChanges;
144
- return {
168
+ var pluginState = _main.syncedBlockPluginKey.getState(editorState);
169
+ var activeFlag = pluginState.activeFlag,
170
+ currentSyncBlockStore = pluginState.syncBlockStore,
171
+ bodiedSyncBlockDeletionStatus = pluginState.bodiedSyncBlockDeletionStatus,
172
+ retryCreationPosMap = pluginState.retryCreationPosMap,
173
+ hasSyncedBlocks = pluginState.hasSyncedBlocks,
174
+ hasUnsavedBodiedSyncBlockChanges = pluginState.hasUnsavedBodiedSyncBlockChanges;
175
+
176
+ // --- EDITOR-6929 / PR-F: return a stable reference when all
177
+ // fields are unchanged to prevent unnecessary React re-renders. ---
178
+ if (cachedSharedState !== undefined && cachedSharedState.activeFlag === activeFlag && cachedSharedState.syncBlockStore === currentSyncBlockStore && cachedSharedState.bodiedSyncBlockDeletionStatus === bodiedSyncBlockDeletionStatus && cachedSharedState.retryCreationPosMap === retryCreationPosMap && cachedSharedState.hasSyncedBlocks === hasSyncedBlocks && cachedSharedState.hasUnsavedBodiedSyncBlockChanges === hasUnsavedBodiedSyncBlockChanges && (0, _expValEquals.expValEquals)('editor_synced_block_perf', 'isEnabled', true)) {
179
+ return cachedSharedState;
180
+ }
181
+ var nextSharedState = {
145
182
  activeFlag: activeFlag,
146
183
  syncBlockStore: currentSyncBlockStore,
147
184
  bodiedSyncBlockDeletionStatus: bodiedSyncBlockDeletionStatus,
@@ -149,6 +186,8 @@ var syncedBlockPlugin = exports.syncedBlockPlugin = function syncedBlockPlugin(_
149
186
  hasSyncedBlocks: hasSyncedBlocks,
150
187
  hasUnsavedBodiedSyncBlockChanges: hasUnsavedBodiedSyncBlockChanges
151
188
  };
189
+ cachedSharedState = nextSharedState;
190
+ return nextSharedState;
152
191
  }
153
192
  };
154
193
  };
@@ -324,7 +324,7 @@ export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api
324
324
  // When the perf gate is ON and the doc has synced blocks we do a
325
325
  // single traversal here; afterwards `apply()` will map or rebuild
326
326
  // only when a status signal changes.
327
- const initStatusDecorationSet = expValEquals('editor_synced_block_perf', 'isEnabled', true) && docHasSyncedBlocks ? buildStatusDecorations(instance.doc, syncBlockStore, initIsOffline, initIsViewMode, initIsDragging) : DecorationSet.empty;
327
+ const initStatusDecorationSet = docHasSyncedBlocks && expValEquals('editor_synced_block_perf', 'isEnabled', true) ? buildStatusDecorations(instance.doc, syncBlockStore, initIsOffline, initIsViewMode, initIsDragging) : DecorationSet.empty;
328
328
  return {
329
329
  selectionDecorationSet: calculateDecorations(instance.doc, instance.selection, instance.schema),
330
330
  activeFlag: false,
@@ -341,6 +341,7 @@ export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api
341
341
  apply: (tr, currentPluginState, oldEditorState) => {
342
342
  var _meta$activeFlag, _meta$bodiedSyncBlock;
343
343
  const meta = tr.getMeta(syncedBlockPluginKey);
344
+ const isPerfExperimentOn = expValEquals('editor_synced_block_perf', 'isEnabled', true);
344
345
  const {
345
346
  activeFlag,
346
347
  selectionDecorationSet,
@@ -356,11 +357,19 @@ export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api
356
357
  // Lazy-init bookkeeping: once a synced block enters the document we
357
358
  // flip `hasSyncedBlocks` to `true` for the lifetime of this editor
358
359
  let nextHasSyncedBlocks = prevHasSyncedBlocks;
359
- if (!prevHasSyncedBlocks && tr.docChanged && expValEquals('editor_synced_block_perf', 'isEnabled', true)) {
360
+ if (!prevHasSyncedBlocks && tr.docChanged && isPerfExperimentOn) {
360
361
  if (transactionInsertsSyncedBlock(tr)) {
361
362
  nextHasSyncedBlocks = true;
362
363
  }
363
364
  }
365
+
366
+ // --- Fast path (EDITOR-6929): when `hasSyncedBlocks` is false,
367
+ // no meta is set, and the selection/doc haven't changed in a way
368
+ // that affects our state, return the SAME object reference so
369
+ // SharedStateAPI skips notifying subscribers. ---
370
+ if (!nextHasSyncedBlocks && !meta && !tr.docChanged && tr.selection.eq(oldEditorState.selection) && isPerfExperimentOn) {
371
+ return currentPluginState;
372
+ }
364
373
  let newDecorationSet = tr.docChanged ? selectionDecorationSet.map(tr.mapping, tr.doc) // only map if document changed
365
374
  : selectionDecorationSet;
366
375
  if (!tr.selection.eq(oldEditorState.selection)) {
@@ -384,7 +393,7 @@ export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api
384
393
  let nextIsOffline = prevOffline;
385
394
  let nextIsViewMode = prevViewMode;
386
395
  let nextIsDragging = prevDragging;
387
- if (expValEquals('editor_synced_block_perf', 'isEnabled', true)) {
396
+ if (isPerfExperimentOn) {
388
397
  if (!nextHasSyncedBlocks) {
389
398
  // No synced blocks → keep empty status decorations
390
399
  nextStatusDecorationSet = DecorationSet.empty;
@@ -413,14 +422,24 @@ export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api
413
422
  }
414
423
  const newPosEntry = meta === null || meta === void 0 ? void 0 : meta.retryCreationPos;
415
424
  const newRetryCreationPosMap = mapRetryCreationPosMap(retryCreationPosMap, newPosEntry, tr.mapping.map.bind(tr.mapping));
425
+ const nextActiveFlag = (_meta$activeFlag = meta === null || meta === void 0 ? void 0 : meta.activeFlag) !== null && _meta$activeFlag !== void 0 ? _meta$activeFlag : activeFlag;
426
+ const nextBodiedSyncBlockDeletionStatus = (_meta$bodiedSyncBlock = meta === null || meta === void 0 ? void 0 : meta.bodiedSyncBlockDeletionStatus) !== null && _meta$bodiedSyncBlock !== void 0 ? _meta$bodiedSyncBlock : bodiedSyncBlockDeletionStatus;
427
+ const nextHasUnsavedBodiedSyncBlockChanges = syncBlockStore.sourceManager.hasUnsavedChanges();
428
+
429
+ // --- Reference equality (EDITOR-6929): return the same object
430
+ // when ALL fields are reference-equal to avoid SharedStateAPI
431
+ // notifying subscribers and triggering React re-renders. ---
432
+ if (nextActiveFlag === activeFlag && newDecorationSet === selectionDecorationSet && newRetryCreationPosMap === retryCreationPosMap && nextHasSyncedBlocks === prevHasSyncedBlocks && nextBodiedSyncBlockDeletionStatus === bodiedSyncBlockDeletionStatus && nextHasUnsavedBodiedSyncBlockChanges === currentPluginState.hasUnsavedBodiedSyncBlockChanges && nextStatusDecorationSet === prevStatusDecorationSet && nextIsOffline === prevOffline && nextIsViewMode === prevViewMode && nextIsDragging === prevDragging && isPerfExperimentOn) {
433
+ return currentPluginState;
434
+ }
416
435
  return {
417
- activeFlag: (_meta$activeFlag = meta === null || meta === void 0 ? void 0 : meta.activeFlag) !== null && _meta$activeFlag !== void 0 ? _meta$activeFlag : activeFlag,
436
+ activeFlag: nextActiveFlag,
418
437
  selectionDecorationSet: newDecorationSet,
419
438
  syncBlockStore: syncBlockStore,
420
439
  retryCreationPosMap: newRetryCreationPosMap,
421
440
  hasSyncedBlocks: nextHasSyncedBlocks,
422
- bodiedSyncBlockDeletionStatus: (_meta$bodiedSyncBlock = meta === null || meta === void 0 ? void 0 : meta.bodiedSyncBlockDeletionStatus) !== null && _meta$bodiedSyncBlock !== void 0 ? _meta$bodiedSyncBlock : bodiedSyncBlockDeletionStatus,
423
- hasUnsavedBodiedSyncBlockChanges: syncBlockStore.sourceManager.hasUnsavedChanges(),
441
+ bodiedSyncBlockDeletionStatus: nextBodiedSyncBlockDeletionStatus,
442
+ hasUnsavedBodiedSyncBlockChanges: nextHasUnsavedBodiedSyncBlockChanges,
424
443
  statusDecorationSet: nextStatusDecorationSet,
425
444
  prevIsOffline: nextIsOffline,
426
445
  prevIsViewMode: nextIsViewMode,
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
2
  import { syncBlock, bodiedSyncBlock } from '@atlaskit/adf-schema';
3
+ import { useSharedPluginStateWithSelector } from '@atlaskit/editor-common/hooks';
3
4
  import { SyncBlockStoreManager } from '@atlaskit/editor-synced-block-provider';
4
5
  import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
5
6
  import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
@@ -15,6 +16,36 @@ import { getToolbarConfig } from './ui/floating-toolbar';
15
16
  import { getQuickInsertConfig } from './ui/quick-insert';
16
17
  import { SyncBlockRefresher } from './ui/SyncBlockRefresher';
17
18
  import { getToolbarComponents } from './ui/toolbar-components';
19
+
20
+ /**
21
+ * EDITOR-6929 / PR-G: Guard contentComponent rendering.
22
+ * When `hasSyncedBlocks` is false return null
23
+ * to avoid mounting SyncBlockRefresher, DeleteConfirmationModal, and Flag —
24
+ * their hooks (useSharedPluginStateWithSelector) would execute selectors on
25
+ * every transaction for no benefit on the ~99.98% of pages with zero synced
26
+ * blocks.
27
+ */
28
+ const LazySyncedBlockUI = ({
29
+ syncBlockStore: syncBlockStoreManager,
30
+ api
31
+ }) => {
32
+ const hasSyncBlocks = useSharedPluginStateWithSelector(api, ['syncedBlock'], states => {
33
+ var _states$syncedBlockSt;
34
+ return (_states$syncedBlockSt = states.syncedBlockState) === null || _states$syncedBlockSt === void 0 ? void 0 : _states$syncedBlockSt.hasSyncedBlocks;
35
+ });
36
+ if (!hasSyncBlocks && expValEquals('editor_synced_block_perf', 'isEnabled', true)) {
37
+ return null;
38
+ }
39
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(SyncBlockRefresher, {
40
+ syncBlockStoreManager: syncBlockStoreManager,
41
+ api: api
42
+ }), /*#__PURE__*/React.createElement(DeleteConfirmationModal, {
43
+ syncBlockStoreManager: syncBlockStoreManager,
44
+ api: api
45
+ }), /*#__PURE__*/React.createElement(Flag, {
46
+ api: api
47
+ }));
48
+ };
18
49
  export const syncedBlockPlugin = ({
19
50
  config,
20
51
  api
@@ -25,6 +56,13 @@ export const syncedBlockPlugin = ({
25
56
  const syncBlockStore = new SyncBlockStoreManager(config === null || config === void 0 ? void 0 : config.syncBlockDataProvider, viewMode, config === null || config === void 0 ? void 0 : config.__livePage);
26
57
  const isPerfExperimentOn = expValEquals('editor_synced_block_perf', 'isEnabled', true);
27
58
  syncBlockStore.setFireAnalyticsEvent(api === null || api === void 0 ? void 0 : (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : (_api$analytics$action = _api$analytics.actions) === null || _api$analytics$action === void 0 ? void 0 : _api$analytics$action.fireAnalyticsEvent);
59
+
60
+ // --- Memoized getSharedState (EDITOR-6929 / PR-F) ---
61
+ // Cache the last returned shared state object. On each call, perform a
62
+ // shallow comparison of all fields against the cached value. If nothing
63
+ // changed, return the cached reference so SharedStateAPI subscribers
64
+ // (React components) skip re-rendering.
65
+ let cachedSharedState;
28
66
  api === null || api === void 0 ? void 0 : (_api$blockMenu = api.blockMenu) === null || _api$blockMenu === void 0 ? void 0 : _api$blockMenu.actions.registerBlockMenuComponents(getBlockMenuComponents(api, (_config$enableSourceC = config === null || config === void 0 ? void 0 : config.enableSourceCreation) !== null && _config$enableSourceC !== void 0 ? _config$enableSourceC : false));
29
67
  api === null || api === void 0 ? void 0 : (_api$toolbar = api.toolbar) === null || _api$toolbar === void 0 ? void 0 : _api$toolbar.actions.registerComponents(getToolbarComponents(api, (_config$enableSourceC2 = config === null || config === void 0 ? void 0 : config.enableSourceCreation) !== null && _config$enableSourceC2 !== void 0 ? _config$enableSourceC2 : false));
30
68
  return {
@@ -108,20 +146,16 @@ export const syncedBlockPlugin = ({
108
146
  refs.containerElement = containerElement || undefined;
109
147
  refs.popupsMountPoint = popupsMountPoint || undefined;
110
148
  refs.wrapperElement = wrapperElement || undefined;
111
- return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(SyncBlockRefresher, {
112
- syncBlockStoreManager: syncBlockStore,
113
- api: api
114
- }), /*#__PURE__*/React.createElement(DeleteConfirmationModal, {
115
- syncBlockStoreManager: syncBlockStore,
149
+ return /*#__PURE__*/React.createElement(LazySyncedBlockUI, {
150
+ syncBlockStore: syncBlockStore,
116
151
  api: api
117
- }), /*#__PURE__*/React.createElement(Flag, {
118
- api: api
119
- }));
152
+ });
120
153
  },
121
154
  getSharedState: editorState => {
122
155
  if (!editorState) {
123
156
  return;
124
157
  }
158
+ const pluginState = syncedBlockPluginKey.getState(editorState);
125
159
  const {
126
160
  activeFlag,
127
161
  syncBlockStore: currentSyncBlockStore,
@@ -129,8 +163,14 @@ export const syncedBlockPlugin = ({
129
163
  retryCreationPosMap,
130
164
  hasSyncedBlocks,
131
165
  hasUnsavedBodiedSyncBlockChanges
132
- } = syncedBlockPluginKey.getState(editorState);
133
- return {
166
+ } = pluginState;
167
+
168
+ // --- EDITOR-6929 / PR-F: return a stable reference when all
169
+ // fields are unchanged to prevent unnecessary React re-renders. ---
170
+ if (cachedSharedState !== undefined && cachedSharedState.activeFlag === activeFlag && cachedSharedState.syncBlockStore === currentSyncBlockStore && cachedSharedState.bodiedSyncBlockDeletionStatus === bodiedSyncBlockDeletionStatus && cachedSharedState.retryCreationPosMap === retryCreationPosMap && cachedSharedState.hasSyncedBlocks === hasSyncedBlocks && cachedSharedState.hasUnsavedBodiedSyncBlockChanges === hasUnsavedBodiedSyncBlockChanges && expValEquals('editor_synced_block_perf', 'isEnabled', true)) {
171
+ return cachedSharedState;
172
+ }
173
+ const nextSharedState = {
134
174
  activeFlag,
135
175
  syncBlockStore: currentSyncBlockStore,
136
176
  bodiedSyncBlockDeletionStatus,
@@ -138,6 +178,8 @@ export const syncedBlockPlugin = ({
138
178
  hasSyncedBlocks,
139
179
  hasUnsavedBodiedSyncBlockChanges
140
180
  };
181
+ cachedSharedState = nextSharedState;
182
+ return nextSharedState;
141
183
  }
142
184
  };
143
185
  };
@@ -350,7 +350,7 @@ export var createPlugin = function createPlugin(options, pmPluginFactoryParams,
350
350
  // When the perf gate is ON and the doc has synced blocks we do a
351
351
  // single traversal here; afterwards `apply()` will map or rebuild
352
352
  // only when a status signal changes.
353
- var initStatusDecorationSet = expValEquals('editor_synced_block_perf', 'isEnabled', true) && docHasSyncedBlocks ? buildStatusDecorations(instance.doc, syncBlockStore, initIsOffline, initIsViewMode, initIsDragging) : DecorationSet.empty;
353
+ var initStatusDecorationSet = docHasSyncedBlocks && expValEquals('editor_synced_block_perf', 'isEnabled', true) ? buildStatusDecorations(instance.doc, syncBlockStore, initIsOffline, initIsViewMode, initIsDragging) : DecorationSet.empty;
354
354
  return {
355
355
  selectionDecorationSet: calculateDecorations(instance.doc, instance.selection, instance.schema),
356
356
  activeFlag: false,
@@ -367,6 +367,7 @@ export var createPlugin = function createPlugin(options, pmPluginFactoryParams,
367
367
  apply: function apply(tr, currentPluginState, oldEditorState) {
368
368
  var _meta$activeFlag, _meta$bodiedSyncBlock;
369
369
  var meta = tr.getMeta(syncedBlockPluginKey);
370
+ var isPerfExperimentOn = expValEquals('editor_synced_block_perf', 'isEnabled', true);
370
371
  var activeFlag = currentPluginState.activeFlag,
371
372
  selectionDecorationSet = currentPluginState.selectionDecorationSet,
372
373
  bodiedSyncBlockDeletionStatus = currentPluginState.bodiedSyncBlockDeletionStatus,
@@ -380,11 +381,19 @@ export var createPlugin = function createPlugin(options, pmPluginFactoryParams,
380
381
  // Lazy-init bookkeeping: once a synced block enters the document we
381
382
  // flip `hasSyncedBlocks` to `true` for the lifetime of this editor
382
383
  var nextHasSyncedBlocks = prevHasSyncedBlocks;
383
- if (!prevHasSyncedBlocks && tr.docChanged && expValEquals('editor_synced_block_perf', 'isEnabled', true)) {
384
+ if (!prevHasSyncedBlocks && tr.docChanged && isPerfExperimentOn) {
384
385
  if (transactionInsertsSyncedBlock(tr)) {
385
386
  nextHasSyncedBlocks = true;
386
387
  }
387
388
  }
389
+
390
+ // --- Fast path (EDITOR-6929): when `hasSyncedBlocks` is false,
391
+ // no meta is set, and the selection/doc haven't changed in a way
392
+ // that affects our state, return the SAME object reference so
393
+ // SharedStateAPI skips notifying subscribers. ---
394
+ if (!nextHasSyncedBlocks && !meta && !tr.docChanged && tr.selection.eq(oldEditorState.selection) && isPerfExperimentOn) {
395
+ return currentPluginState;
396
+ }
388
397
  var newDecorationSet = tr.docChanged ? selectionDecorationSet.map(tr.mapping, tr.doc) // only map if document changed
389
398
  : selectionDecorationSet;
390
399
  if (!tr.selection.eq(oldEditorState.selection)) {
@@ -408,7 +417,7 @@ export var createPlugin = function createPlugin(options, pmPluginFactoryParams,
408
417
  var nextIsOffline = prevOffline;
409
418
  var nextIsViewMode = prevViewMode;
410
419
  var nextIsDragging = prevDragging;
411
- if (expValEquals('editor_synced_block_perf', 'isEnabled', true)) {
420
+ if (isPerfExperimentOn) {
412
421
  if (!nextHasSyncedBlocks) {
413
422
  // No synced blocks → keep empty status decorations
414
423
  nextStatusDecorationSet = DecorationSet.empty;
@@ -437,14 +446,24 @@ export var createPlugin = function createPlugin(options, pmPluginFactoryParams,
437
446
  }
438
447
  var newPosEntry = meta === null || meta === void 0 ? void 0 : meta.retryCreationPos;
439
448
  var newRetryCreationPosMap = mapRetryCreationPosMap(retryCreationPosMap, newPosEntry, tr.mapping.map.bind(tr.mapping));
449
+ var nextActiveFlag = (_meta$activeFlag = meta === null || meta === void 0 ? void 0 : meta.activeFlag) !== null && _meta$activeFlag !== void 0 ? _meta$activeFlag : activeFlag;
450
+ var nextBodiedSyncBlockDeletionStatus = (_meta$bodiedSyncBlock = meta === null || meta === void 0 ? void 0 : meta.bodiedSyncBlockDeletionStatus) !== null && _meta$bodiedSyncBlock !== void 0 ? _meta$bodiedSyncBlock : bodiedSyncBlockDeletionStatus;
451
+ var nextHasUnsavedBodiedSyncBlockChanges = syncBlockStore.sourceManager.hasUnsavedChanges();
452
+
453
+ // --- Reference equality (EDITOR-6929): return the same object
454
+ // when ALL fields are reference-equal to avoid SharedStateAPI
455
+ // notifying subscribers and triggering React re-renders. ---
456
+ if (nextActiveFlag === activeFlag && newDecorationSet === selectionDecorationSet && newRetryCreationPosMap === retryCreationPosMap && nextHasSyncedBlocks === prevHasSyncedBlocks && nextBodiedSyncBlockDeletionStatus === bodiedSyncBlockDeletionStatus && nextHasUnsavedBodiedSyncBlockChanges === currentPluginState.hasUnsavedBodiedSyncBlockChanges && nextStatusDecorationSet === prevStatusDecorationSet && nextIsOffline === prevOffline && nextIsViewMode === prevViewMode && nextIsDragging === prevDragging && isPerfExperimentOn) {
457
+ return currentPluginState;
458
+ }
440
459
  return {
441
- activeFlag: (_meta$activeFlag = meta === null || meta === void 0 ? void 0 : meta.activeFlag) !== null && _meta$activeFlag !== void 0 ? _meta$activeFlag : activeFlag,
460
+ activeFlag: nextActiveFlag,
442
461
  selectionDecorationSet: newDecorationSet,
443
462
  syncBlockStore: syncBlockStore,
444
463
  retryCreationPosMap: newRetryCreationPosMap,
445
464
  hasSyncedBlocks: nextHasSyncedBlocks,
446
- bodiedSyncBlockDeletionStatus: (_meta$bodiedSyncBlock = meta === null || meta === void 0 ? void 0 : meta.bodiedSyncBlockDeletionStatus) !== null && _meta$bodiedSyncBlock !== void 0 ? _meta$bodiedSyncBlock : bodiedSyncBlockDeletionStatus,
447
- hasUnsavedBodiedSyncBlockChanges: syncBlockStore.sourceManager.hasUnsavedChanges(),
465
+ bodiedSyncBlockDeletionStatus: nextBodiedSyncBlockDeletionStatus,
466
+ hasUnsavedBodiedSyncBlockChanges: nextHasUnsavedBodiedSyncBlockChanges,
448
467
  statusDecorationSet: nextStatusDecorationSet,
449
468
  prevIsOffline: nextIsOffline,
450
469
  prevIsViewMode: nextIsViewMode,
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
2
  import { syncBlock, bodiedSyncBlock } from '@atlaskit/adf-schema';
3
+ import { useSharedPluginStateWithSelector } from '@atlaskit/editor-common/hooks';
3
4
  import { SyncBlockStoreManager } from '@atlaskit/editor-synced-block-provider';
4
5
  import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
5
6
  import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
@@ -15,15 +16,51 @@ import { getToolbarConfig } from './ui/floating-toolbar';
15
16
  import { getQuickInsertConfig } from './ui/quick-insert';
16
17
  import { SyncBlockRefresher } from './ui/SyncBlockRefresher';
17
18
  import { getToolbarComponents } from './ui/toolbar-components';
18
- export var syncedBlockPlugin = function syncedBlockPlugin(_ref) {
19
- var _api$editorViewMode, _api$analytics, _api$blockMenu, _config$enableSourceC, _api$toolbar, _config$enableSourceC2;
20
- var config = _ref.config,
19
+
20
+ /**
21
+ * EDITOR-6929 / PR-G: Guard contentComponent rendering.
22
+ * When `hasSyncedBlocks` is false return null
23
+ * to avoid mounting SyncBlockRefresher, DeleteConfirmationModal, and Flag —
24
+ * their hooks (useSharedPluginStateWithSelector) would execute selectors on
25
+ * every transaction for no benefit on the ~99.98% of pages with zero synced
26
+ * blocks.
27
+ */
28
+ var LazySyncedBlockUI = function LazySyncedBlockUI(_ref) {
29
+ var syncBlockStoreManager = _ref.syncBlockStore,
21
30
  api = _ref.api;
31
+ var hasSyncBlocks = useSharedPluginStateWithSelector(api, ['syncedBlock'], function (states) {
32
+ var _states$syncedBlockSt;
33
+ return (_states$syncedBlockSt = states.syncedBlockState) === null || _states$syncedBlockSt === void 0 ? void 0 : _states$syncedBlockSt.hasSyncedBlocks;
34
+ });
35
+ if (!hasSyncBlocks && expValEquals('editor_synced_block_perf', 'isEnabled', true)) {
36
+ return null;
37
+ }
38
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(SyncBlockRefresher, {
39
+ syncBlockStoreManager: syncBlockStoreManager,
40
+ api: api
41
+ }), /*#__PURE__*/React.createElement(DeleteConfirmationModal, {
42
+ syncBlockStoreManager: syncBlockStoreManager,
43
+ api: api
44
+ }), /*#__PURE__*/React.createElement(Flag, {
45
+ api: api
46
+ }));
47
+ };
48
+ export var syncedBlockPlugin = function syncedBlockPlugin(_ref2) {
49
+ var _api$editorViewMode, _api$analytics, _api$blockMenu, _config$enableSourceC, _api$toolbar, _config$enableSourceC2;
50
+ var config = _ref2.config,
51
+ api = _ref2.api;
22
52
  var refs = {};
23
53
  var viewMode = api === null || api === void 0 || (_api$editorViewMode = api.editorViewMode) === null || _api$editorViewMode === void 0 || (_api$editorViewMode = _api$editorViewMode.sharedState.currentState()) === null || _api$editorViewMode === void 0 ? void 0 : _api$editorViewMode.mode;
24
54
  var syncBlockStore = new SyncBlockStoreManager(config === null || config === void 0 ? void 0 : config.syncBlockDataProvider, viewMode, config === null || config === void 0 ? void 0 : config.__livePage);
25
55
  var isPerfExperimentOn = expValEquals('editor_synced_block_perf', 'isEnabled', true);
26
56
  syncBlockStore.setFireAnalyticsEvent(api === null || api === void 0 || (_api$analytics = api.analytics) === null || _api$analytics === void 0 || (_api$analytics = _api$analytics.actions) === null || _api$analytics === void 0 ? void 0 : _api$analytics.fireAnalyticsEvent);
57
+
58
+ // --- Memoized getSharedState (EDITOR-6929 / PR-F) ---
59
+ // Cache the last returned shared state object. On each call, perform a
60
+ // shallow comparison of all fields against the cached value. If nothing
61
+ // changed, return the cached reference so SharedStateAPI subscribers
62
+ // (React components) skip re-rendering.
63
+ var cachedSharedState;
27
64
  api === null || api === void 0 || (_api$blockMenu = api.blockMenu) === null || _api$blockMenu === void 0 || _api$blockMenu.actions.registerBlockMenuComponents(getBlockMenuComponents(api, (_config$enableSourceC = config === null || config === void 0 ? void 0 : config.enableSourceCreation) !== null && _config$enableSourceC !== void 0 ? _config$enableSourceC : false));
28
65
  api === null || api === void 0 || (_api$toolbar = api.toolbar) === null || _api$toolbar === void 0 || _api$toolbar.actions.registerComponents(getToolbarComponents(api, (_config$enableSourceC2 = config === null || config === void 0 ? void 0 : config.enableSourceCreation) !== null && _config$enableSourceC2 !== void 0 ? _config$enableSourceC2 : false));
29
66
  return {
@@ -65,9 +102,9 @@ export var syncedBlockPlugin = function syncedBlockPlugin(_ref) {
65
102
  return copySyncedBlockReferenceToClipboardEditorCommand(syncBlockStore, inputMethod, api);
66
103
  },
67
104
  insertSyncedBlock: function insertSyncedBlock() {
68
- return function (_ref2) {
105
+ return function (_ref3) {
69
106
  var _api$analytics3;
70
- var tr = _ref2.tr;
107
+ var tr = _ref3.tr;
71
108
  if (!(config !== null && config !== void 0 && config.enableSourceCreation)) {
72
109
  return null;
73
110
  }
@@ -106,35 +143,36 @@ export var syncedBlockPlugin = function syncedBlockPlugin(_ref) {
106
143
  return getToolbarConfig(state, intl, api, syncBlockStore);
107
144
  }
108
145
  },
109
- contentComponent: function contentComponent(_ref3) {
110
- var containerElement = _ref3.containerElement,
111
- wrapperElement = _ref3.wrapperElement,
112
- popupsMountPoint = _ref3.popupsMountPoint;
146
+ contentComponent: function contentComponent(_ref4) {
147
+ var containerElement = _ref4.containerElement,
148
+ wrapperElement = _ref4.wrapperElement,
149
+ popupsMountPoint = _ref4.popupsMountPoint;
113
150
  refs.containerElement = containerElement || undefined;
114
151
  refs.popupsMountPoint = popupsMountPoint || undefined;
115
152
  refs.wrapperElement = wrapperElement || undefined;
116
- return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(SyncBlockRefresher, {
117
- syncBlockStoreManager: syncBlockStore,
118
- api: api
119
- }), /*#__PURE__*/React.createElement(DeleteConfirmationModal, {
120
- syncBlockStoreManager: syncBlockStore,
153
+ return /*#__PURE__*/React.createElement(LazySyncedBlockUI, {
154
+ syncBlockStore: syncBlockStore,
121
155
  api: api
122
- }), /*#__PURE__*/React.createElement(Flag, {
123
- api: api
124
- }));
156
+ });
125
157
  },
126
158
  getSharedState: function getSharedState(editorState) {
127
159
  if (!editorState) {
128
160
  return;
129
161
  }
130
- var _syncedBlockPluginKey2 = syncedBlockPluginKey.getState(editorState),
131
- activeFlag = _syncedBlockPluginKey2.activeFlag,
132
- currentSyncBlockStore = _syncedBlockPluginKey2.syncBlockStore,
133
- bodiedSyncBlockDeletionStatus = _syncedBlockPluginKey2.bodiedSyncBlockDeletionStatus,
134
- retryCreationPosMap = _syncedBlockPluginKey2.retryCreationPosMap,
135
- hasSyncedBlocks = _syncedBlockPluginKey2.hasSyncedBlocks,
136
- hasUnsavedBodiedSyncBlockChanges = _syncedBlockPluginKey2.hasUnsavedBodiedSyncBlockChanges;
137
- return {
162
+ var pluginState = syncedBlockPluginKey.getState(editorState);
163
+ var activeFlag = pluginState.activeFlag,
164
+ currentSyncBlockStore = pluginState.syncBlockStore,
165
+ bodiedSyncBlockDeletionStatus = pluginState.bodiedSyncBlockDeletionStatus,
166
+ retryCreationPosMap = pluginState.retryCreationPosMap,
167
+ hasSyncedBlocks = pluginState.hasSyncedBlocks,
168
+ hasUnsavedBodiedSyncBlockChanges = pluginState.hasUnsavedBodiedSyncBlockChanges;
169
+
170
+ // --- EDITOR-6929 / PR-F: return a stable reference when all
171
+ // fields are unchanged to prevent unnecessary React re-renders. ---
172
+ if (cachedSharedState !== undefined && cachedSharedState.activeFlag === activeFlag && cachedSharedState.syncBlockStore === currentSyncBlockStore && cachedSharedState.bodiedSyncBlockDeletionStatus === bodiedSyncBlockDeletionStatus && cachedSharedState.retryCreationPosMap === retryCreationPosMap && cachedSharedState.hasSyncedBlocks === hasSyncedBlocks && cachedSharedState.hasUnsavedBodiedSyncBlockChanges === hasUnsavedBodiedSyncBlockChanges && expValEquals('editor_synced_block_perf', 'isEnabled', true)) {
173
+ return cachedSharedState;
174
+ }
175
+ var nextSharedState = {
138
176
  activeFlag: activeFlag,
139
177
  syncBlockStore: currentSyncBlockStore,
140
178
  bodiedSyncBlockDeletionStatus: bodiedSyncBlockDeletionStatus,
@@ -142,6 +180,8 @@ export var syncedBlockPlugin = function syncedBlockPlugin(_ref) {
142
180
  hasSyncedBlocks: hasSyncedBlocks,
143
181
  hasUnsavedBodiedSyncBlockChanges: hasUnsavedBodiedSyncBlockChanges
144
182
  };
183
+ cachedSharedState = nextSharedState;
184
+ return nextSharedState;
145
185
  }
146
186
  };
147
187
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/editor-plugin-synced-block",
3
- "version": "8.2.10",
3
+ "version": "8.2.11",
4
4
  "description": "SyncedBlock plugin for @atlaskit/editor-core",
5
5
  "author": "Atlassian Pty Ltd",
6
6
  "license": "Apache-2.0",
@@ -55,7 +55,7 @@
55
55
  "@atlaskit/primitives": "^19.0.0",
56
56
  "@atlaskit/spinner": "19.1.2",
57
57
  "@atlaskit/tmp-editor-statsig": "^77.3.0",
58
- "@atlaskit/tokens": "13.0.3",
58
+ "@atlaskit/tokens": "13.0.4",
59
59
  "@atlaskit/tooltip": "^22.0.0",
60
60
  "@atlaskit/visually-hidden": "^3.1.0",
61
61
  "@babel/runtime": "^7.0.0",