Create a Custom WooCommerce Product Loop (The Right Way)

- 42 comments

Development,Projects,WooCommerce,WordPress,WordPress Filters

If you ever needed to create a custom WooCommerce Product loop then you probably used get_posts() or even a WP_Query(). But did you know that using those could actually cause your loop to stop working unexpectedly or even break your site? If you didn’t know that then I’m guessing you also didn’t know that WooCommerce actually has a native function for doing this. It’s ok, I didn’t know any of that either until I wanted to create a custom WooCommerce Product loop complete with all its addons and paging without repeating any code.

Skip to the final snippet

On a recent WooCommerce project I was tasked with building a page to display only Featured Products. WooCommerce actually has a handy function to make getting Featured Product IDs very quick, wc_get_featured_product_ids(). Using that, it’s fairly trivial to plug that in to a custom WP_Query() and fire out a vanilla custom loop (or use get_posts() if you’re determined to do it the long way). I could even use the Featured Products shortcode, but those methods aren’t ideal. Unfortunately, anyone searching for the right way to accomplish this will find no shortage of stackexchange posts offering WP_Query() and get_posts() solutions. Let’s talk about why those aren’t great and what the best practices are.

The Downsides of using WP_Query & Shortcodes

The downside to a custom WooCommerce loop using any of the methods above is that I’d end up losing the WooCommerce goodies that are baked into WooCommerce’s native loop. I wouldn’t get a nifty ordering dropdown to sort products by various criteria, and I wouldn’t get my much needed pagination. I can add those elements back into the loop myself, but it’s a good amount of repetitive code I’d like to avoid adding to the project, especially because WooCommerce already comes with that code. In addition, if I’ve used any of the WooCommerce action or filter hooks to change the main WooCommerce loop behavior or appearance then I’ll need to account for those changes in my custom loop.

One more important downside is mentioned in the WooCommerce GitHub Wiki (emphasis added):

Building custom WP_Queries or database queries is likely to break your code in future versions of WooCommerce as data moves towards custom tables for better performance. This is the best-practices way for plugin and theme developers to retrieve multiple products.

After reading that and considering the other downsides of using WP_Query() and other methods to create a custom WooCommerce product loop, I decided to dig around and devise the most Woo-native way of creating a custom product loop to keep all the WooCommerce loop features while still leaving room to make the customizations I need. In my case, and the example I’ll be demonstrating, that means displaying only Featured Products on a page made to closely resemble the main Shop page.

A Better Way

My quest for a better WooCommerce custom loop started where all great copies start: the original. So I took a look at archive-product.php inside my WooCommerce installation and began analyzing it. It’s actually very simple, but it’s also very nuanced. So I’m going to follow the structure of the WooCommerce Archive Template, including its action hooks and filters (along with a few other important bits I’ll need to add), but instead of using the WordPress Archive loop (which isn’t available in a standard page template) I’ll use wc_get_products() to follow best practices mentioned in the Wiki above.

Here’s what we’re starting with:

This is the simple base we’re starting with. It’s a very basic WordPress loop with a few action hooks and WooCommerce functions. Let’s dig in to how we can replicate this with our custom loop.

Getting Started

The first thing I did was make a Page Template. If you found this post then I assume you already know how to do that or you can use that link to figure it out. Next, I’ll copy the entire loop from the WooCommerce Product Archive template. However, I need to make some adjustments because that loop relies on the WordPress main Post Type Archive query. That query won’t be available on a Page (hence the need for a custom query/loop).

The Custom WooCommerce Product Loop

I’ll be using wc_get_products() as recommended on the WooCommerce Wiki so I need to adjust my code because that function (and even the WC_Product_Query Class) doesn’t give me the methods we’re all used to when creating custom loops, namely have_posts(). Instead of relying on have_posts() and the_post(), I’ll create a foreach loop. My project’s main Shop Page makes use of the WooCommerce ordering dropdown and pagination, so I’ll need to get some of the relevant variables to ensure those functions work here, and I also need to set some properties in the WooCommerce loop variable ($GLOBAL['woocommerce_loop']). Here’s what I came up with:

It’s actually fairly straightforward. First I get and set the variables for paging and order display in lines 7-11, then I get my Products, then I hack the WooCommerce loop variable with some parameters it needs in lines 25-30, then I finally loop through them using a foreach loop instead of the usual while loop because I don’t have the Loop methods to rely on as mentioned above. Lines 36-38 use the same form and function to temporarily set the global Post object as WooCommerce uses in its shortcodes, but hit the comments if there’s a better way.

Conclusion

Creating a custom WooCommerce Product loop on a Page in WordPress is often mentioned in tutorials and almost always calls for using a new WP_Query() or get_posts(). Both of those methods are certainly viable, but ultimately they may not keep pace with WooCommerce updates and so the right way to create a custom product loop is to use wc_get_products() and a foreach loop or WC_Product_Query(). It’s also possible through a few quick tricks to persuade WooCommerce into outputting its goodies like ordering dropdowns and pagination to save you some time and save your project from repetitive code. This method can be used just about anywhere in your templates and does a thorough job of outputting a WooCommerce Product loop that matches up with the default Product loop on your Shop and Product Category pages. Any changes you’ve made to the WooCommerce loop’s action hooks or filters will be output when using this snippet so you won’t need to repeat yourself.

I hope this helps more WordPress and WooCommerce developers implement custom Product loops the right way. If you have questions or comments or noticed something I may have overlooked please leave a comment below!


Comments

  1. Hello and thank you for the post!
    The pagination while it is displayed, it doesn’t work.
    It redirects to page/2/, but it shows 404 error

  2. wc_set_loop_prop('total', $featured_products->total); produces an error: Trying to get property 'total' of non-object

  3. Thanks for this. But for some reason original query mixed with the new one(Im changing a tempate for one specific category and custom loop shows all subcatgories first and then displays custom query results) Plus pagintaion doesn’t work on page one. But when i add ‘page/2/’ to url – paginatin works.

  4. Hey Michael, great post thanks! it came up top on the search results.

    I am using latest woocommerce and storefront. When I select the ‘sort by average rating’ the code breaks, because what is returned is an array and not a string.

    print_r( $ordering ):
    <em>Array ( [orderby] => Array ( [meta_value_num] => DESC [ID] => ASC ) [order] => ASC [meta_key] => _wc_average_rating )</em>
    
    Warning: explode() expects parameter 2 to be string, array given in /home/pall/public_html/wp-content/themes/storefront-child/template-custom.php on line 46
    
    Warning: array_shift() expects parameter 1 to be array, null given in /home/pall/public_html/wp-content/themes/storefront-child/template-custom.php on line 46
    
  5. Thanks for this great content. But I do have one question about using AJAX with this method to handle my own custom page pagination and filters for the products. I would usually do something like creating a wp_query to get the post data store it in a variable then covert it into JSON and pass it to my AJAX function to output to the page. But how would I be able to pass all the product data to my ajax function using this method or would it be best to use wp_query in this situation? I can provide an example on how I would do this with wp_query if I wasn’t clear enough. Thanks

    1. The function and class in the post are basically wrappers for get_posts() and WP_Query, so you should be able to use the Woo function/class just fine. Have you tried it? Specifically what “product data” do you think you’ll need that you can’t fetch with these Woo functions?

  6. Great article, very very helpful.
    However, I’m trying to use this to only show products of certain categories. Would that be possible ? Do I have to change the $featured_products array to only call the category I need? Thanks for any help you might give me.

    1. Hi Antonin, glad you found this helpful. For a category, you can omit the featured argument and instead use the category argument, which accepts an array of categories by slug. So something like category => array('category-slug-1', 'another-category-slug') should work.

          1. Never came across the wiki before! Thanks for that!
            I wanted to ask another question, you might have a quick answer, if not no worries. But I’ve been trying to add a dropdown filter menu for attributes to appear in the woocommerce_before_shop_loop ( so that it sits next to the catalog ordering dropdown).
            By creating a custom widget region and using the native product attribute filter widget I was able to get it to work. The issue is that the widget only works on the archive and shop page, and I want it to work on any page. There’s a link that I can’t seem to find.
            Tried looking for ressources on how to create a custom attri filter dropdown from scratch but no luck there either.
            Just wondering if you have a quick tip to give me.
            Again thanks so much for your time and your help.

  7. I hope you can help me. I’m trying to get a shortcode to expand on the Shop page within the product short description text.

    The shortcode is added to the product short description text box. It expands as expected on the single product page and using the quick view but not on the Shop page.

    I think I need to edit the archive-product.php file in my child theme but I’m not sure how/what to edit to get it to work.

    Any help would be greatly appreciated.

  8. Hey Michael, excellent tutorial.

    Do you have any other article where explains how exactly does the WC loop works? I’m having a trouble with that.

    Thanks in advance.

    1. Hi Edinaldo, thank you for the kind words. The WC loop is just a basic WordPress loop which you can read more about here: https://codex.wordpress.org/The_Loop. WC adds a few action hooks and has some variables of its own that it uses to setup the loop, but apart from that it’s a standard WordPress loop. If you let me know what you’re stuck on I can try to shed some light where the Codex might be unclear.

      1. Thanks for replying. Alright, i’ll try to be clear as possible.

        In my home page i need to display only the featured products on the top and, below this all the products are displayed with a excerpt of it’s description limited to 50 chars and the button to add to the cart. Then, in another page i need to create a filter for all products categories, displaying only the products that matches with the filter.

        I’m using the Outlet child theme for Storefront.
        Thanks already

        1. What have you tried so far and what issues are you having? If you link to a Gist of your template files and let me know specifically where you’re having difficulty then I can try to help more.

  9. Hi, thank you for the tutorial, it was great, but I have a problem. I want to show products with custom fields ( like 5 of them ) in products archives or product category
    Is it possible?
    Like brand and color or everything important about my products
    Thank you for your time.

    1. Thanks Dave. Not sure what you mean, but the function is actually wp_reset_postdata(), so make sure you’re calling it correctly. In order to troubleshoot more, I’d need to see your code. Also, you can try commenting out any other custom functions in your functions.php that may be customizing/affecting your Page Template or the WooCommerce loop.

  10. Very good read! Thank you. However when I use above code some pages contain 11 products, some 12 and some 13. How could I force the number to be 12 at all times?

    1. Great tutorial. but as Robbert is saying, the first pages is showing 1 product less than the following pages. how can we solve this?

      1. Dave, glad it’s helpful, but as I’ve replied to everyone else who reported problems in the comments, I’d need to see the entire template you’re using this code in to be able to pinpoint any possible issues. Even then, there may be hooks in use in your functions.php, plugins, or elsewhere that are affecting the loop. Your best bet is to post the complete template code in a Gist, or debug some of the loop variables you’re using, including running print_r() on the custom query variable itself to ensure it’s returning the correct number of posts.

  11. Love the tutorial, really helped! I have a further query. I am using the above code in a site where I need to make a custom loop on several pages. So far I have this working thanks to your great tutorial. What I need to do now is filter one of them for products that are on sale. I was looking at another example of this that uses WP_Query and they use a meta query with an OR operator to test for sales price > 0 for both simple and variable products. Could you suggest how I would achieve this using the loop above?

    1. Ryan, thanks for the kind words, I’m glad this helped you. What you want to do should actually be quite easy. Fortunately, the function I alluded to in the intro—wc_get_featured_product_ids()—has a cousin called wc_get_product_ids_on_sale() (see here), so you’re in luck 🙂 For future reference, that’s a great file full of useful functions. So you can try something like this for your loop arguments:

        $sale_products           = wc_get_products(array(
          'meta_key'             => '_price',
          'status'               => 'publish',
          'limit'                => $products_per_page,
          'page'                 => $paged,
          'include'              => wc_get_product_ids_on_sale(),
          'paginate'             => true,
          'return'               => 'ids',
          'orderby'              => $ordering['orderby'],
          'order'                => $ordering['order'],
        ));
      

      Notice that all I did was remove the 'featured' => true and replaced it with 'include' => wc_get_product_ids_on_sale(). I hope that helps.

    1. Sure Lucas, can you please post your full template source code into a GitHub Gist or a StackExchange question? I’ll be happy to take a look.

  12. This is a great piece of code! I am having trouble with pagination though. I get a 404 error when moving past page 1.

    Any thoughts? Thanks!

    1. Thanks John. You can try flushing your permalinks (just visiting Settings -> Permalinks in your admin will do that). If that doesn’t work, post your entire template’s code here or in a Gist and I’ll have a look.

Leave a Reply to Max Harris

Click here to cancel reply.