Commit 685197ff559 for woocommerce

commit 685197ff5593bd904b87c5a235d79472ae68860b
Author: Tung Du <dinhtungdu@gmail.com>
Date:   Thu Jun 11 10:13:13 2026 +0700

    Fix Product Filters sync across multiple instances (#65622)

    * fix: sync active filters with server

    * test: add product filters sync e2e

    * fix: lock product filters store

    * fix: address product filters review feedback

    * chore: changelog

diff --git a/plugins/woocommerce/changelog/fix-synced-product-filters-instances b/plugins/woocommerce/changelog/fix-synced-product-filters-instances
new file mode 100644
index 00000000000..0b8f21f4068
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-synced-product-filters-instances
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix: Sync active filters across multiple Product Filters blocks on the same page.
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/block.json b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/block.json
index 2d0ef2f6add..212e91f61d1 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/block.json
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/block.json
@@ -15,7 +15,6 @@
 			"enableContrastChecker": false,
 			"button": true
 		},
-		"multiple": false,
 		"inserter": true,
 		"interactivity": true,
 		"typography": {
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/constants.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/constants.ts
index 6c26a0f9c2b..61dccfe5f72 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/constants.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/constants.ts
@@ -1,5 +1,9 @@
+export const PRODUCT_FILTERS_STORE_NAME = 'woocommerce/product-filters';
+export const PRODUCT_FILTERS_STORE_LOCK =
+	'I acknowledge that using a private store means my plugin will inevitably break on the next store release.';
+
 export const EXCLUDED_BLOCKS = [
-	'woocommerce/product-filters',
+	PRODUCT_FILTERS_STORE_NAME,
 	'woocommerce/product-filter-attribute',
 	'woocommerce/product-filter-active',
 	'woocommerce/product-filter-price',
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/frontend.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/frontend.ts
index 63a6b6dff8e..120023fa8f4 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/frontend.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/frontend.ts
@@ -15,10 +15,14 @@ import type {
 	ProductFiltersContext,
 } from './types';
 import { getClosestColor } from './utils/get-closest-color';
+import {
+	PRODUCT_FILTERS_STORE_LOCK,
+	PRODUCT_FILTERS_STORE_NAME,
+} from './constants';

 const { getContext, getElement, store, getServerContext, getConfig } = iAPI;

-const BLOCK_NAME = 'woocommerce/product-filters';
+const BLOCK_NAME = PRODUCT_FILTERS_STORE_NAME;

 type ValidFilterOptionItem = FilterOptionItem & {
 	type: string;
@@ -77,7 +81,6 @@ function unselectFilter( item: ValidFilterOptionItem ) {
 const productFiltersStore = {
 	state: {
 		get params() {
-			const { activeFilters } = getContext< ProductFiltersContext >();
 			const params: Record< string, string > = {};

 			function addParam( key: string, value: string ) {
@@ -89,7 +92,7 @@ const productFiltersStore = {
 			const config = getConfig( BLOCK_NAME );
 			const taxonomyParamsMap = config?.taxonomyParamsMap || {};

-			activeFilters.forEach( ( filter ) => {
+			state.activeFilters.forEach( ( filter ) => {
 				// todo: refactor this to use params data from Automattic\WooCommerce\Internal\ProductFilters\Params.
 				const { type, value } = filter;

@@ -199,7 +202,7 @@ const productFiltersStore = {
 					? itemArg
 					: context.item;
 			if ( ! item || ! isValidFilterOptionItem( item ) ) return;
-			const isSelected = context.activeFilters.some(
+			const isSelected = state.activeFilters.some(
 				( f ) => f.type === item.type && f.value === item.value
 			);
 			if ( isSelected ) {
@@ -302,6 +305,15 @@ const productFiltersStore = {
 				document.body.style.overflow = 'auto';
 			}
 		},
+		syncActiveFiltersWithServer: () => {
+			if ( ! getServerContext ) return;
+			const context = getContext< ProductFiltersContext >();
+			const serverContext = getServerContext< ProductFiltersContext >();
+
+			context.activeFilters = Array.isArray( serverContext.activeFilters )
+				? serverContext.activeFilters.map( ( item ) => ( { ...item } ) )
+				: [];
+		},
 	},
 };

@@ -313,5 +325,6 @@ export type ProductFiltersStore = typeof productFiltersStore;

 const { state, actions } = store< ProductFiltersStore >(
 	BLOCK_NAME,
-	productFiltersStore
+	productFiltersStore,
+	{ lock: PRODUCT_FILTERS_STORE_LOCK }
 );
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/active-filters/frontend.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/active-filters/frontend.ts
index 2f2f94df122..c8c6c68e22a 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/active-filters/frontend.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/active-filters/frontend.ts
@@ -12,6 +12,10 @@ import type {
 	RemovableItem,
 	RemovableItemsParentStore,
 } from '../../../../types/type-defs/removable-items';
+import {
+	PRODUCT_FILTERS_STORE_LOCK,
+	PRODUCT_FILTERS_STORE_NAME,
+} from '../../constants';

 type RemovableItemContext = {
 	item: RemovableItem;
@@ -19,16 +23,13 @@ type RemovableItemContext = {

 const activeFiltersStore = {
 	state: {
-		get removableItems() {
-			const { activeFilters } = getContext< ProductFiltersContext >();
-			return activeFilters
-				.filter( ( f ) => !! f.value )
-				.map( ( f ) => ( {
-					id: f.type + '_' + f.value,
-					type: f.type,
-					value: f.value,
-					label: f.activeLabel,
-				} ) );
+		get removableItems(): RemovableItem[] {
+			return state.activeFilters.map( ( f ) => ( {
+				id: f.type + '_' + f.value,
+				type: f.type,
+				value: f.value,
+				label: f.activeLabel,
+			} ) );
 		},
 		get removeItemLabel() {
 			const { item } = getContext< RemovableItemContext >();
@@ -40,9 +41,8 @@ const activeFiltersStore = {
 			const label = typeof item?.label === 'string' ? item.label : '';
 			return template.replace( '{{label}}', label );
 		},
-		get hasActiveFilters() {
-			const { activeFilters } = getContext< ProductFiltersContext >();
-			return activeFilters.length > 0;
+		get hasActiveFilters(): boolean {
+			return state.activeFilters.length > 0;
 		},
 	},
 	actions: {
@@ -66,7 +66,8 @@ const activeFiltersStore = {
 // eslint-disable-next-line @typescript-eslint/no-unused-expressions
 activeFiltersStore satisfies RemovableItemsParentStore;

-const { actions } = store< ProductFiltersStore & typeof activeFiltersStore >(
-	'woocommerce/product-filters',
-	activeFiltersStore
-);
+const { state, actions } = store<
+	ProductFiltersStore & typeof activeFiltersStore
+>( PRODUCT_FILTERS_STORE_NAME, activeFiltersStore, {
+	lock: PRODUCT_FILTERS_STORE_LOCK,
+} );
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/frontend.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/frontend.ts
index 8e702ae4363..17d7ab4cd38 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/frontend.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/frontend.ts
@@ -15,6 +15,10 @@ import {
 	isVisualAttributeTermEmpty,
 } from '../../../../base/utils/visual-attribute-terms';
 import type { VisualAttributeTerm } from '../../../../base/utils/visual-attribute-terms';
+import {
+	PRODUCT_FILTERS_STORE_LOCK,
+	PRODUCT_FILTERS_STORE_NAME,
+} from '../../constants';

 type CheckboxListItem = SelectableItem< {
 	visual?: VisualAttributeTerm;
@@ -44,6 +48,11 @@ type CheckboxListStore = {

 function getParentStore( storeNamespace?: string ) {
 	if ( ! storeNamespace ) return undefined;
+	if ( storeNamespace === PRODUCT_FILTERS_STORE_NAME ) {
+		return store<
+			SelectableItemsParentStore< { visual?: VisualAttributeTerm } >
+		>( storeNamespace, undefined, { lock: PRODUCT_FILTERS_STORE_LOCK } );
+	}
 	return store<
 		SelectableItemsParentStore< { visual?: VisualAttributeTerm } >
 	>( storeNamespace );
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/chips/frontend.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/chips/frontend.ts
index 309910ccfde..223abbc9316 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/chips/frontend.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/chips/frontend.ts
@@ -16,6 +16,10 @@ import {
 } from '../../../../base/utils/visual-attribute-terms';
 import type { VisualAttributeTerm } from '../../../../base/utils/visual-attribute-terms';
 import { getClosestColor } from '../../utils/get-closest-color';
+import {
+	PRODUCT_FILTERS_STORE_LOCK,
+	PRODUCT_FILTERS_STORE_NAME,
+} from '../../constants';

 type ChipsItem = SelectableItem< {
 	visual?: VisualAttributeTerm;
@@ -49,6 +53,11 @@ type ChipsStore = {

 function getParentStore( storeNamespace?: string ) {
 	if ( ! storeNamespace ) return undefined;
+	if ( storeNamespace === PRODUCT_FILTERS_STORE_NAME ) {
+		return store<
+			SelectableItemsParentStore< { visual?: VisualAttributeTerm } >
+		>( storeNamespace, undefined, { lock: PRODUCT_FILTERS_STORE_LOCK } );
+	}
 	return store<
 		SelectableItemsParentStore< { visual?: VisualAttributeTerm } >
 	>( storeNamespace );
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/price-filter/frontend.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/price-filter/frontend.ts
index 15346432bb8..320920fd01d 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/price-filter/frontend.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/price-filter/frontend.ts
@@ -11,6 +11,10 @@ import type { ProductFiltersContext } from '../../types';
 import type { ProductFiltersStore } from '../../frontend';
 import { formatPrice, getCurrency } from '../../utils/price-currency';
 import type { RangeInputParentStore } from '../../../../types/type-defs/range-input';
+import {
+	PRODUCT_FILTERS_STORE_LOCK,
+	PRODUCT_FILTERS_STORE_NAME,
+} from '../../constants';

 const { store, getContext, getServerContext, getConfig } = iAPI;

@@ -25,12 +29,11 @@ export type ProductFilterPriceContext = {

 const productFilterPriceStore = {
 	state: {
-		get minPrice() {
-			const { activeFilters } = getContext< ProductFiltersContext >();
+		get minPrice(): number {
 			const { minRange } = getServerContext
 				? getServerContext< ProductFilterPriceContext >()
 				: getContext< ProductFilterPriceContext >();
-			const priceFilter = activeFilters.find(
+			const priceFilter = state.activeFilters.find(
 				( filter ) => filter.type === 'price'
 			);
 			if ( priceFilter ) {
@@ -39,12 +42,11 @@ const productFilterPriceStore = {
 			}
 			return minRange;
 		},
-		get maxPrice() {
-			const { activeFilters } = getContext< ProductFiltersContext >();
+		get maxPrice(): number {
 			const { maxRange } = getServerContext
 				? getServerContext< ProductFilterPriceContext >()
 				: getContext< ProductFilterPriceContext >();
-			const priceFilter = activeFilters.find(
+			const priceFilter = state.activeFilters.find(
 				( filter ) => filter.type === 'price'
 			);
 			if ( priceFilter ) {
@@ -181,4 +183,6 @@ export type ProductFilterPriceStore = typeof productFilterPriceStore;

 const { state, actions } = store<
 	ProductFiltersStore & ProductFilterPriceStore
->( 'woocommerce/product-filters', productFilterPriceStore );
+>( PRODUCT_FILTERS_STORE_NAME, productFilterPriceStore, {
+	lock: PRODUCT_FILTERS_STORE_LOCK,
+} );
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/price-slider/frontend.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/price-slider/frontend.ts
index b2b7575530c..fed17cc173a 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/price-slider/frontend.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/price-slider/frontend.ts
@@ -12,6 +12,10 @@ import type {
 	ProductFilterPriceContext,
 	ProductFilterPriceStore,
 } from '../price-filter/frontend';
+import {
+	PRODUCT_FILTERS_STORE_LOCK,
+	PRODUCT_FILTERS_STORE_NAME,
+} from '../../constants';

 const { store, getContext, getElement, withScope, getServerContext } = iAPI;

@@ -73,4 +77,6 @@ const { state, actions } = store<
 	ProductFiltersStore &
 		ProductFilterPriceStore &
 		typeof productFilterPriceSliderStore
->( 'woocommerce/product-filters', productFilterPriceSliderStore );
+>( PRODUCT_FILTERS_STORE_NAME, productFilterPriceSliderStore, {
+	lock: PRODUCT_FILTERS_STORE_LOCK,
+} );
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/content-templates/template_archive-product_multiple-product-filters.handlebars b/plugins/woocommerce/client/blocks/tests/e2e/content-templates/template_archive-product_multiple-product-filters.handlebars
new file mode 100644
index 00000000000..08e378ebf53
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/tests/e2e/content-templates/template_archive-product_multiple-product-filters.handlebars
@@ -0,0 +1,42 @@
+<!-- wp:woocommerce/product-filters -->
+<div class="wp-block-woocommerce-product-filters wc-block-product-filters" style="--wc-product-filters-text-color:#111;--wc-product-filters-background-color:#fff">
+	<!-- wp:woocommerce/product-filter-active -->
+	<div class="wp-block-woocommerce-product-filter-active">
+		<!-- wp:woocommerce/product-filter-removable-chips {"lock":{"remove":true}} -->
+		<div class="wp-block-woocommerce-product-filter-removable-chips wc-block-product-filter-removable-chips"></div>
+		<!-- /wp:woocommerce/product-filter-removable-chips -->
+	</div>
+	<!-- /wp:woocommerce/product-filter-active -->
+</div>
+<!-- /wp:woocommerce/product-filters -->
+
+<!-- wp:woocommerce/product-filters -->
+<div class="wp-block-woocommerce-product-filters wc-block-product-filters" style="--wc-product-filters-text-color:#111;--wc-product-filters-background-color:#fff">
+{{#> wp-block blockName='woocommerce/product-filter-attribute' attributes=attributes }}
+<div class="wp-block-woocommerce-product-filter-attribute"><!-- wp:group {"metadata":{"name":"Header"},"style":{"spacing":{"blockGap":"0"}},"layout":{"type":"flex","flexWrap":"nowrap"}} -->
+<div class="wp-block-group"><!-- wp:heading {"level":3} -->
+<h3 class="wp-block-heading">Attribute</h3>
+<!-- /wp:heading --></div>
+<!-- /wp:group -->
+
+<!-- wp:woocommerce/product-filter-checkbox-list {"lock":{"remove":true}} -->
+<div class="wp-block-woocommerce-product-filter-checkbox-list wc-block-product-filter-checkbox-list"></div>
+<!-- /wp:woocommerce/product-filter-checkbox-list --></div>
+{{/ wp-block }}
+</div>
+<!-- /wp:woocommerce/product-filters -->
+
+<!-- wp:woocommerce/product-collection {"query":{"perPage":9,"pages":0,"offset":0,"postType":"product","order":"asc","orderBy":"title","search":"","exclude":[],"inherit":true,"taxQuery":[],"isProductCollectionBlock":true,"featured":false,"woocommerceOnSale":false,"woocommerceStockStatus":["instock","outofstock","onbackorder"],"woocommerceAttributes":[],"woocommerceHandPickedProducts":[]},"tagName":"div","displayLayout":{"type":"flex","columns":3,"shrinkColumns":true},"queryContextIncludes":["collection"]} -->
+<div class="wp-block-woocommerce-product-collection">
+	<!-- wp:woocommerce/product-template -->
+	<!-- wp:woocommerce/product-image {"showSaleBadge":false,"imageSizing":"thumbnail","isDescendentOfQueryLoop":true} -->
+		<!-- wp:woocommerce/product-sale-badge {"isDescendentOfQueryLoop":true,"align":"right"} /-->
+	<!-- /wp:woocommerce/product-image -->
+	<!-- wp:post-title {"textAlign":"center","level":3,"isLink":true,"style":{"spacing":{"margin":{"bottom":"0.75rem","top":"0"}}},"fontSize":"medium","__woocommerceNamespace":"woocommerce/product-collection/product-title"} /-->
+
+	<!-- wp:woocommerce/product-price {"isDescendentOfQueryLoop":true,"textAlign":"center","fontSize":"small"} /-->
+
+	<!-- wp:woocommerce/product-button {"textAlign":"center","isDescendentOfQueryLoop":true,"fontSize":"small"} /-->
+	<!-- /wp:woocommerce/product-template -->
+</div>
+<!-- /wp:woocommerce/product-collection -->
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/tests/product-filters/product-filters-frontend.block_theme.spec.ts b/plugins/woocommerce/client/blocks/tests/e2e/tests/product-filters/product-filters-frontend.block_theme.spec.ts
index 0f90453cced..73c2152334d 100644
--- a/plugins/woocommerce/client/blocks/tests/e2e/tests/product-filters/product-filters-frontend.block_theme.spec.ts
+++ b/plugins/woocommerce/client/blocks/tests/e2e/tests/product-filters/product-filters-frontend.block_theme.spec.ts
@@ -117,4 +117,75 @@ test.describe( 'woocommerce/product-filters - Frontend', () => {
 			await expect( products ).toHaveCount( 2 );
 		} );
 	} );
+
+	test.describe( 'Multiple instances', () => {
+		test.beforeEach( async ( { page } ) => {
+			await page.addInitScript( () => {
+				// Mock the wc global variable.
+				if ( typeof window.wc === 'undefined' ) {
+					window.wc = {
+						wcSettings: {
+							getSetting() {
+								return true;
+							},
+						},
+					};
+				}
+			} );
+		} );
+
+		test( 'syncs active filters between Product Filters blocks', async ( {
+			page,
+			requestUtils,
+		} ) => {
+			const templateCompiler = await requestUtils.createTemplateFromFile(
+				'archive-product_multiple-product-filters'
+			);
+
+			await templateCompiler.compile( {
+				attributes: {
+					attributeId: 1,
+				},
+			} );
+
+			await page.goto( '/shop' );
+
+			const productFilters = page.locator(
+				'.wp-block-woocommerce-product-filters'
+			);
+			await expect( productFilters ).toHaveCount( 2 );
+
+			const activeFiltersBlock = productFilters.first();
+			const filterOptionsBlock = productFilters.nth( 1 );
+			const grayCheckbox = filterOptionsBlock.getByRole( 'checkbox', {
+				name: 'Gray',
+			} );
+			const grayActiveFilter = activeFiltersBlock.getByText(
+				'Color: Gray',
+				{ exact: true }
+			);
+
+			await expect( grayActiveFilter ).toBeHidden();
+
+			await grayCheckbox.click();
+			await page.waitForURL(
+				( url ) => url.searchParams.get( 'filter_color' ) === 'gray'
+			);
+
+			await expect( grayActiveFilter ).toBeVisible();
+			await expect( grayCheckbox ).toBeChecked();
+
+			await activeFiltersBlock
+				.getByRole( 'button', {
+					name: 'Remove filter: Color: Gray',
+				} )
+				.click();
+			await page.waitForURL(
+				( url ) => ! url.searchParams.has( 'filter_color' )
+			);
+
+			await expect( grayActiveFilter ).toBeHidden();
+			await expect( grayCheckbox ).not.toBeChecked();
+		} );
+	} );
 } );
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilters.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilters.php
index 82bd379c55e..3f2ce89ac69 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilters.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilters.php
@@ -116,6 +116,7 @@ class ProductFilters extends AbstractBlock {
 			'data-wp-interactive'              => $this->get_full_block_name(),
 			'data-wp-init--colors'             => 'callbacks.initColors',
 			'data-wp-watch--scrolling'         => 'callbacks.scrollLimit',
+			'data-wp-watch--active-filters'    => 'callbacks.syncActiveFiltersWithServer',
 			'data-wp-on--keyup'                => 'actions.closeOverlayOnEscape',
 			'data-wp-context'                  => (string) wp_json_encode( $interactivity_context, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ),
 			'data-wp-class--is-overlay-opened' => 'context.isOverlayOpened',