{"id":19018559,"date":"2026-09-12T20:10:17","date_gmt":"2026-09-12T20:10:17","guid":{"rendered":"https:\/\/wordpress.org\/support\/topic\/mutationobserver-breaks-acf-flexible-content-collapsed-state-memory\/"},"modified":"2026-09-12T20:10:17","modified_gmt":"2026-09-12T20:10:17","slug":"mutationobserver-breaks-acf-flexible-content-collapsed-state-memory","status":"publish","type":"topic","link":"https:\/\/wordpress.org\/support\/topic\/mutationobserver-breaks-acf-flexible-content-collapsed-state-memory\/","title":{"rendered":"MutationObserver breaks ACF flexible content collapsed-state memory"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Plugin 1.4.0 \u00b7 ACF PRO 6.8.10 \u00b7 WordPress 7.1 Summary<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">GREAT plugin, first off.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">With this plugin active, ACF&#8217;s flexible content field loses its collapsed-layout memory:<br \/>every layout renders expanded on each page load. Deactivating the plugin restores the<br \/>behavior immediately. The page I&#8217;m editing has ~56 flexible content layouts, so this is<br \/>the difference between a usable editor screen and an unusable one. Steps to reproduce<\/p>\n\n\n\n<ol>\n<li>A post type with an ACF PRO flexible content field holding many layouts.<\/li>\n\n\n\n<li>At least one of those layouts contains a Table field.<\/li>\n\n\n\n<li>Collapse some or all layouts. Reload the page.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Expected: layouts return collapsed.<br \/>Actual: every layout is expanded again. What I found<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">ACF PRO stores flexible content collapse state in localStorage under the key &#8220;acf&#8221;,<br \/>as <code>collapsedLayouts-&lt;postID&gt;<\/code>, handled by the <code>this.collapsedLayouts<\/code> model in<br \/>assets\/build\/js\/pro\/acf-pro-input.min.js. That model builds its storage key like this:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>key: function(key, context) {        \/\/ context = \"load\" or \"save\"\n    var count = this.get(key + context) || 0;\n    count++;\n    this.set(key + context, count, true);\n    if (count &gt; 1) key += '-' + count;\n    return key;\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">So if the same field asks for its state more than once in a single page load, the<br \/>second request gets a DIFFERENT key with a &#8220;-2&#8221; suffix. Load and save keep separate<br \/>counters, so reads and writes can end up on opposite sides of that suffix.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That is exactly what I see in localStorage with the plugin active:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>collapsedLayouts-1511: {\n    field_610ac7050d4ff:   Array(56),\n    field_610ac7050d4ff-2: Array(55)\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">One field, two entries. With the plugin deactivated, only the unsuffixed key appears<br \/>and collapse state persists correctly. Suspected cause<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">js\/init.js attaches a MutationObserver to the entire document, unconditionally, whether<br \/>or not a Table field is present on the screen:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>mutationObserver.observe( document.documentElement, {\n    childList: true,\n    subtree: true,\n});<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Each batch re-arms a 250ms timer that calls update_tables() -&gt; each_table(), which then<br \/>mutates the DOM itself (adds classes, injects .acf-table-wrap) and so re-triggers its own<br \/>observer. On an ACF admin screen with a large flexible content field, that churn overlaps<br \/>the window in which ACF initializes the field, and the field ends up initialized more than<br \/>once \u2014 which mints the suffixed key above.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">I have not instrumented the double initialization directly; that part is inferred from<br \/>ACF&#8217;s key-suffixing behavior plus the observed &#8220;-2&#8221; entry. What is directly confirmed is<br \/>that the &#8220;-2&#8221; key appears only while this plugin is active. Suggested fix<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Scope the observer rather than watching the whole document:<\/p>\n\n\n\n<ul>\n<li>observe only a container that can actually contain table fields, or<\/li>\n\n\n\n<li>skip attaching it entirely when no <code>.acf-table-root<\/code> exists on the page, or<\/li>\n\n\n\n<li>prefer ACF&#8217;s own <code>append<\/code> \/ <code>ready<\/code> actions over a MutationObserver, which is how ACF<br \/>expects add-ons to learn about new fields, and<\/li>\n\n\n\n<li>optionally offer a constant to opt out, in the spirit of the existing<br \/>ACF_TABLEFIELD_FILTER_POSTMETA.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Happy to test a patch against the above setup.<\/p>\n","protected":false},"template":"","class_list":["post-19018559","topic","type-topic","status-publish","hentry","topic-tag-acf","topic-tag-collapse","topic-tag-flexible-content","topic-tag-localstorage"],"jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/wordpress.org\/support\/wp-json\/wp\/v2\/topic\/19018559","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/wordpress.org\/support\/wp-json\/wp\/v2\/topic"}],"about":[{"href":"https:\/\/wordpress.org\/support\/wp-json\/wp\/v2\/types\/topic"}],"version-history":[{"count":0,"href":"https:\/\/wordpress.org\/support\/wp-json\/wp\/v2\/topic\/19018559\/revisions"}],"wp:attachment":[{"href":"https:\/\/wordpress.org\/support\/wp-json\/wp\/v2\/media?parent=19018559"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}