Block Editor: Allow stable block IDs in block editor store#74687
Block Editor: Allow stable block IDs in block editor store#74687youknowriad merged 1 commit intotrunkfrom
Conversation
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
|
Size Change: +252 B (+0.01%) Total Size: 3.09 MB
ℹ️ View Unchanged
|
Implement internal/external client ID mapping in useBlockSync hook to preserve stable external block IDs while using unique internal IDs in the store. This allows features like real-time collaboration and template navigation to maintain consistent block references. The hook now: - Clones blocks with new internal IDs using cloneBlockWithMapping - Maintains bidirectional mapping between external and internal IDs - Restores external IDs before calling onChange/onInput callbacks - Translates selection client IDs for inner block controllers Fixes #74623 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
6ef5ee8 to
7c80481
Compare
|
@youknowriad Thank you for creating a PR for this issue! I did some local testing, and with the way we currently use block IDs this doesn't solve our problem. I think it could with a way to query internal IDs via store or in the DOM itself. Here are the two places we use block IDs in awareness:
In both cases we have the "real" block ID as an input, and we want to identify the block within the DOM for drawing. Assuming I'm understanding this PR correctly, this approach alone won't work because we're still creating unique block IDs within the DOM when cloning. In "Show Template" mode we still don't know how to locate cloned blocks from their non-cloned IDs. I have a couple of ideas on how to get closer to what we're hoping for:
Open to other ideas for changing how we approach this from the drawing/plugin side if you have any suggestions. |
|
@alecgeatches Quering the DOM is very fragile, why don't you use the "selection" state in core-data instead? |
|
@alecgeatches, the |
|
@Mamaduka I think for his use-case we shouldn't rely on the internal uids of the block-editor store instead it's better to use the more persistent ones (core-data) which would be more persistent/permanent. |
Thank you for the quick response! I don't fully understand these comments, but I think I'm missing something on how we can use block IDs once they're cloned, and where core-data comes in. Just so we're on the same page, here are some concrete visual examples of how things usually work: Selection in RTC-synced blocksBlocks in real-time collaboration have synced client IDs:
This makes it simple to communicate between clients. Below, the left-side user listens for selection change events from the block editor store and announces to other collaborators "I'm in block This process for collaborators is pretty easy. User 2 knows where the Selection in "Show Template" blocksWhen user 2 switches into "Show Template" mode, or loads a post with that mode enabled, those block IDs change: Screen.Recording.2026-01-20.at.11.27.09.AM.movUser 2's block ID is now Is there a way for user 2 to use that information? How do I take the selection data that user 1 is sending me and know which block they're talking about and where I should draw? The problem exists in the opposite direction as well - user 2 starts announcing "I'm in block Thank you! I imagine I'm missing something here. |
|
AFAIK, syncing works at the So my feeling is that it makes sense to always sync from the same level: sync also the awareness (selection and all) from the same store (core-data) instead of syncing some data (blocks) from core-data and other (selection) from another state (DOM or block-editor store). This likely leads to ambiguity and issues because there's no guarantee that these match conceptually. Now, the question is whether we have all the necessary data needed to be synced in "core-data" store and the reality is that I don't really know. I know we have a "selection" object per entity that represents the cursor position basically within a given entity. I think what we might be missing though is in the case of an entity (think a reusable block) used in two different places, which "instance" of that reusable block has the selection in it. (potentially an information we could "save" in the container's selection object) I know I'm talking a bit in the abstract here, but I think it's the best way to arrive to the best solution. If you hope that using two reusable blocks in a block editor will lead to the same clientids used in both positions, this is never going to happen I think because it would be a huge change and at the same time, it won't solve your issues because you won't really know which one of these two reusable blocks is selected when two reusable blocks are used in the editor. |
Agreed.
Right, but, starting from the right principle that we should sync at the same level, we can work together to see what can be done in Gutenberg to support third-party collaborative experiences.
This is where you lost me. :)
|
Each
Each reusable block is basically a new |
|
Stupid question that I'm sure has been answered somewhere in earlier issues / explorations.... But what is keeping us from just storing the identifier as an actual key in the metadata attribute of blocks? Just in the HTML comment we already strip out whilst rendering the block? |
This is a bit unrelated to the current PR IMO. I wish we had a document for it but it's something that has been discussed several times. @mtias reiterated multiple times that it would basically pollute the post content too much I think. But seems like something worth having documented more thoughtfully. |
|
Assuming there's no blockers, I'll be merging this PR soon. |
@youknowriad The discussion has been a bit over my head, so I just wanted to double-check that (for the purposes of RTC) we won't have a way to correlate one user's selected block ID with another user's if either is cloned? If not, would you consider a |
I think it's possible if you stop relying on DOM and start relying on the "selection" object in core-data for the awareness. If you rely on the rendered block uids, I'm afraid it's not possible. The same block can be present twice and the block editor needs to differentiate between these. |
I can't speak to whether that will solve the collaborative editing problem or not, but for the purpose of keeping track of the selected block when navigating to another entity and back (which is what #73737 does), this will only work if the selection object state is persisted beyond the current screen, which I don't believe is the case? And this PR doesn't solve the clientId inconsistency in the DOM, so we still can't viably substitute the path-based approach in #73737 with clientIds. I'm probably missing something here 😅 but I don't understand how this will work. |
I think we can but I'll look at that after this PR is merged. |
Sorry, if forgot, why do we do this again? |
| // IDs for features like real-time collaboration while using unique | ||
| // internal IDs in the block-editor store. | ||
| const blocksForParent = clientId | ||
| ? restoreExternalIds( blocks, idMappingRef.current ) |
There was a problem hiding this comment.
What happens if the block tree is different, like extra blocks added or removed? Not sure if I'm following this core correctly.
Also, why are there two mappings, just for convenience?
There was a problem hiding this comment.
If the block tree is different, we just use the clientIds that are new, we only keep the previous one if the block is still in the block tree.
There was a problem hiding this comment.
And yes, two maps is for performance reasons, to be able to get the id in both sides without looping.
Imagine you have the same pattern in two places in the block editor, you need to know which one of the two the user has selected... for several reasons (insertions, selection indicator..., all the block-editor selectors basically) |
Co-authored-by: youknowriad <youknowriad@git.wordpress.org> Co-authored-by: ellatrix <ellatrix@git.wordpress.org> Co-authored-by: alecgeatches <alecgeatches@git.wordpress.org> Co-authored-by: Mamaduka <mamaduka@git.wordpress.org> Co-authored-by: mcsf <mcsf@git.wordpress.org> Co-authored-by: fabiankaegy <fabiankaegy@git.wordpress.org> Co-authored-by: tellthemachines <isabel_brison@git.wordpress.org>
Co-authored-by: youknowriad <youknowriad@git.wordpress.org> Co-authored-by: ellatrix <ellatrix@git.wordpress.org> Co-authored-by: alecgeatches <alecgeatches@git.wordpress.org> Co-authored-by: Mamaduka <mamaduka@git.wordpress.org> Co-authored-by: mcsf <mcsf@git.wordpress.org> Co-authored-by: fabiankaegy <fabiankaegy@git.wordpress.org> Co-authored-by: tellthemachines <isabel_brison@git.wordpress.org>


What?
Closes #74623
Implement internal/external client ID mapping in
useBlockSynchook to preserve stable external block IDs while using unique internal IDs in the store.Why?
Block client IDs are not stable within templates because blocks are cloned with randomized UUIDs. This breaks real-time collaboration and template navigation features that rely on consistent block references.
How?
The hook now maintains a bidirectional mapping between external (original) and internal (cloned) client IDs:
cloneBlockWithMapping: Clones blocks while building the mappingrestoreExternalIds: Restores external IDs before calling onChange/onInputrestoreSelectionIds: Translates selection client IDs for callbacksWhen
onChange/onInputis called for inner block controllers, blocks are transformed to use their original external IDs.Testing Instructions
All existing tests pass (1504 tests in block-editor package). Two new tests verify:
Run:
npm run test:unit -- --testPathPattern="use-block-sync"