"1 in cart" on a product nobody added: WooCommerce 11.3 fixes bundles
Picture a garden furniture store. When a shopper adds a table, a custom snippet automatically adds the matching mounting kit. The shopper goes back to the product grid, and the kit’s button reads “1 in cart”. They never added it. At best, they wonder what happened. At worst, they think it is a duplicate, remove the kit, and receive a table they cannot assemble.
The same flaw affects every bundle, composite product or forced sale extension on a block-based store. WooCommerce 11.3 finally provides a way to say “this line belongs to another one”. Here is what changes, and how to apply it to your own linked sales.
Where the wrong count comes from
The counter in question belongs to the Product Button block, used in block-based grids (the Product Collection block) and inside the Add to Cart with Options block. If your store runs on an FSE theme, that is the button under every product.
To compute “N in cart”, the block adds up the cart lines that match the product. WooCommerce core has no way to know that a line was added by an extension on behalf of another one: to core, a line is a line, whether the shopper picked it or not. The only way to fix the count is for the extension to declare the relationship itself.
11.3 also fixes a second flaw along the way. When the same product sat on several cart lines (two different personalizations, for example), the button only showed the quantity of the first line. It now adds up all of them.
The new WooCommerce 11.3 filter
WooCommerce 11.3 adds the woocommerce_store_api_cart_item_parent_item_key filter, and a parent_item_key field in the cart items returned by the Store API. The field is read-only and defaults to null. When it holds the key of another line, the Product Button stops counting that line.
The filter runs once per line, on every Store API response. It receives the current value (null, or whatever another extension already returned) and the cart item array. For a child line, you return the parent’s cart item key. Be careful: this is the key returned by WC()->cart->add_to_cart(), a string like a87ff679a2f3e71d..., not the parent product ID. For every other line, return the value you received untouched: if you always return null, you wipe out another extension’s declaration.
WooCommerce also cleans up on its own. A key that matches no line, or that is the line’s own key, is replaced with null. And the field is only filled while the parent is in the cart: if the shopper removes the table, the kit becomes a standalone line and counts again. No need to clear the stored key.
Who is affected
Every extension that adds cart lines on behalf of another one: bundles, composite products, mix and match, forced sales, chained products, “frequently bought together” blocks. For marketplace extensions, adopting the filter is up to their vendor. The test is quick: add the parent product to the cart, then look at a child product’s button on a block grid. If it reads “1 in cart”, the extension has not caught up yet.
For free gifts and upsells, the answer is less clear-cut. An automatically added gift can perfectly well count as “in cart”; that is an interface decision more than a bug.
Doing nothing breaks nothing: lines keep being counted as they are today. But with my clients, this kind of logic is rarely a marketplace extension. It is a twenty-line snippet written three years ago that nobody will ever update. That is the one to revisit.
A custom linked sale, declared properly
Back to the table and its mounting kit. Three pieces: add the kit while storing the table’s key, declare the relationship to the Store API, and remove the kit if the table leaves the cart (WooCommerce does not do it for you, it only makes the kit standalone).
/**
* Linked sale: product => accessory added automatically.
* Adjust the IDs to your catalog.
*/
const WBA_LINKED_SALES = array(
123 => 456, // Garden table => mounting kit.
);
const WBA_PARENT_KEY = '_wba_parent_key';
// 1. Parent added: add the accessory and store the parent's cart item key.
add_action( 'woocommerce_add_to_cart', function ( $cart_item_key, $product_id, $quantity, $variation_id, $variation, $cart_item_data ) {
if ( isset( $cart_item_data[ WBA_PARENT_KEY ] ) ) {
return; // This is the accessory itself, stop here.
}
$accessory_id = WBA_LINKED_SALES[ $product_id ] ?? 0;
if ( ! $accessory_id ) {
return;
}
WC()->cart->add_to_cart( $accessory_id, $quantity, 0, array(), array( WBA_PARENT_KEY => $cart_item_key ) );
}, 10, 6 );
// 2. Declare the relationship to the Store API (WooCommerce 11.3+).
add_filter( 'woocommerce_store_api_cart_item_parent_item_key', function ( $parent_item_key, $cart_item ) {
return $cart_item[ WBA_PARENT_KEY ] ?? $parent_item_key;
}, 10, 2 );
// 3. Parent leaves the cart: remove its accessories.
add_action( 'woocommerce_cart_item_removed', function ( $removed_key, $cart ) {
foreach ( $cart->get_cart() as $key => $item ) {
if ( ( $item[ WBA_PARENT_KEY ] ?? '' ) === $removed_key ) {
$cart->remove_cart_item( $key );
}
}
}, 10, 2 );
A few details that matter. The parent key is part of the kit line’s data, so WooCommerce keeps it in the session with no extra work. It also feeds into the cart item ID: a kit the shopper adds themselves, on top of the one that came with the table, sits on a separate line, and that is the only one the button will count.
Both actions also fire when the add or the removal goes through the Store API, so the code works with block buttons as well as classic pages. On a version older than 11.3, the filter is never called: the rest of the code works, only the count stays wrong.
One limitation worth knowing, and I checked it in the WooCommerce source. If the shopper adds a second table, the existing line goes to 2, but the kit does not always follow: the classic cart fires woocommerce_add_to_cart again in that case, the Store API does not (it updates the quantity and stops there). Same thing when the shopper changes the quantity from the cart page. If your linked sales must stay in sync, that is a separate job, with its own validation rules (I covered those in the article on Cart block quantity limits).
Why bother now
Today, only the Product Button reads this field. The WooCommerce team says the Cart and Mini-Cart blocks might use it later to show children under their parent. Nothing is promised, but a relationship declared today will be ready the day it lands, and the count is right from now on.
In practice
Look in your theme and custom plugins for add_to_cart() calls triggered from another add to cart (a grep -rn "add_to_cart(" wp-content/themes wp-content/plugins/my-plugin is enough to spot them). Each one is a candidate. Move to 11.3 on a staging site, store the parent key, hook the filter, and run the grid test again. The full announcement is on the WooCommerce developer blog.
Do you have custom bundles or linked sales and want to know whether they hold up with the Cart and Checkout blocks? Get in touch.
Need a WooCommerce store audit?
I'll send you a personalized report with the top priorities to improve. Free, no strings attached.