Fetch WordPress Posts according to custom query with load more button

Solution:1

This is how I load new posts onto the same page using the existing custom WP_Query().

This method also works even with custom url vars which modify the WP_Query().

See my comments in code provided below.

Here is the query php with loops and load more button…

<?php 

// exclude post id's (default false)
$excludePosts = isset($_REQUEST['excludePosts']) ? $_REQUEST['excludePosts'] : false;

// posts per page and load more posts to add
$postsPerPage = 20;

// products query args
$args = [
  'post_type'      => 'products',
  'post_status'    => 'publish',
  'orderby'        => 'ID',
  'order'          => 'DESC',
  'posts_per_page' => $postsPerPage,

  // here is the trick, exclude current posts from query when using jQuery $.post()
  'post__not_in'   => $excludePosts
];

// products query
$products = new WP_Query($args);

?>

<?php // we need this, a parent container for products ?>
<div id="products_archive">

  <?php // if we have query results ?>
  <?php if( $products->have_posts() ): ?>

    <?php // loop our query results (products) ?>
    <?php while( $products->have_posts()): $products->the_post() ?>

      <?php // your product with a data-product attribute and ID as the value ?>
      <?php // we need data-product to find all the currently loaded product ID's ?>
      <article data-product="<?=get_the_id()?>">
        <?php the_title(); ?>
        <?php the_content(); ?>
      </article>

    <?php endwhile; ?>
    
    <?php // if we have more posts to load based on current query, then show button ?>
    <?php if( $products->max_num_pages > 1 ): ?>

      <button id="load_more_products">
        Load more products
      </button>

    <?php endif; ?>

  <?php // if we have no query results ?>
  <?php else: ?>

    Sorry no products found.
  
  <?php endif; ?>

</div>

<?php // we need this, to pass the posts per page value to external jQuery script ?>
<script>
  window.postsPerPage = '<?=$postsPerPage?>';
</script>

Here is the jQuery script using $.post() to reload the current page query excluding products which have already loaded.

// load more products button click event
$(document).on('click', '#load_more_products', function() {

  // our click button
  let $btn = $(this);

  // disable button (you can add spinner to button here too)
  $btn.prop('disabled', true);

  // set current products empty array
  let currentProducts = [];

  // loop thought trough all products in products in archive
  $('[data-product]', '#products_archive').each( function(key, elem) {

    // current product push elements to current products array
    currentProducts.push( $(elem).data('product') );

  });

  // re post current page running the query again
  $.post('', {

    // pass this data to our php var $excludePosts to modify the page WP_Query
    excludePosts: currentProducts

  // html param passed data is full html of our reloaded page
  }, function(html) {

    // find all the product article elements via data-product attribute in html 
    let $newProducts = $('#products_archive [data-product]', html);

    // if new products length is less than our window.postsPerPage variable value
    if( $newProducts.length < postsPerPage ) {

      // then hide the load more button
      $btn.hide();

    }

    // insert new products after last current data-product article
    $newProducts.insertAfter( $('[data-product]', '#products_archive').last() );

  })

  // once our new products have been inserted
  .done(function() {

    // do stuff here to lazy load images etc...

    // re-enable button (and stop button spinner if you have one)
    $btn.prop('disabled', false);

  });

});

Depending on the structure of your current HTML and the placement/appendage of newly fetched products html, you will need to adjust script to suit.