Secondary Queries & Resetting

How new WP_Query, get_posts and query_posts differ, why query_posts breaks the page, and when to reset.

Syntax$q = new WP_Query( $args ); while ( $q->have_posts() ) { $q->the_post(); } wp_reset_postdata();

A secondary query is any post query that is not the main query for the request. Sidebars, related posts, homepage blocks and shortcodes all need one. There are three ways to write it and only two of them are correct.

The three options

new WP_Query( $args ) creates an independent object, leaves the main query alone, and gives you found_posts and max_num_pages. get_posts( $args ) is a thin wrapper around WP_Query that returns WP_Post[] (or int[] with 'fields' => 'ids') and no loop. It forces 'no_found_rows' => true and 'ignore_sticky_posts' => true, and defaults 'suppress_filters' to true, so the posts_where and posts_join filters that plugins rely on are skipped. Its page-size argument is numberposts, which defaults to 5.

query_posts() replaces the main query wholesale. The Code Reference states it "isn't intended for use by plugins or themes". It re-runs the query on a page that has already run one, corrupts the conditional tags and pagination, and breaks anything reading $wp_query afterwards. It is not formally deprecated, but treat it as if it were.

Resetting

the_post() overwrites the global $post, so a secondary loop leaves the page pointing at the wrong post. wp_reset_postdata() restores $post to the main query's current post and is the longhand-free equivalent of $wp_query->reset_postdata(). wp_reset_query() is the heavier one that also restores $wp_query — only needed after query_posts().

Example

Example · php
<?php
// A "more from this author" block inside single.php.
$more_by_author = new WP_Query(
	array(
		'author'         => get_the_author_meta( 'ID' ),
		'post_type'      => 'post',
		'posts_per_page' => 3,
		'post__not_in'   => array( get_the_ID() ), // exclude the post being viewed
		'no_found_rows'  => true, // no pagination here, so skip the row count
	)
);

if ( $more_by_author->have_posts() ) : ?>
	<aside>
		<h3><?php esc_html_e( 'More from this author', 'your-theme' ); ?></h3>
		<ul>
			<?php while ( $more_by_author->have_posts() ) : $more_by_author->the_post(); ?>
				<li>
					<a href="<?php echo esc_url( get_permalink() ); ?>">
						<?php the_title(); ?>
					</a>
				</li>
			<?php endwhile; ?>
		</ul>
	</aside>
	<?php
	// Without this, the_title() further down the page prints the last
	// sidebar post instead of the post being viewed.
	wp_reset_postdata();
endif;

When to use it

  • Add a related-posts or recent-posts block to a single post template
  • Build a shortcode that lists posts without disturbing the page it sits on
  • Populate an admin dropdown with post titles using get_posts()
  • Replace a legacy query_posts() call in an inherited theme

More examples

get_posts() shorthand and the WP_Query longhand it wraps

Reach for get_posts() when you want data — titles, IDs, a dropdown — and not rendered post content. Reach for WP_Query when you need template tags, an accurate found_posts, or the filters that suppress_filters would otherwise disable.

Example · php
<?php
// Shorthand: get_posts() returns an array, no loop needed.
$posts = get_posts(
	array(
		'post_type'   => 'post',
		'numberposts' => 3, // get_posts' own name for posts_per_page; default 5
	)
);

// Longhand: roughly what get_posts() runs internally.
$q = new WP_Query(
	array(
		'post_type'           => 'post',
		'posts_per_page'      => 3,
		'no_found_rows'       => true, // forced by get_posts()
		'ignore_sticky_posts' => true, // forced by get_posts()
		'suppress_filters'    => true, // get_posts' default
	)
);
$posts = $q->posts;

// Iterating an array does not touch the global $post, so no reset is needed:
foreach ( $posts as $p ) {
	echo esc_html( get_the_title( $p ) );
}

Why query_posts() breaks the page

You will find query_posts() in older themes and in copied snippets. Replace it: a secondary query becomes a WP_Query instance, and a change to what the page itself lists becomes a pre_get_posts callback, which modifies the main query before it runs instead of after.

Example · php
<?php
// WRONG. This throws away the main query and runs a second one.
// Conditional tags, pagination and $wp_query are now wrong for the rest
// of the request, and the original query was executed for nothing.
query_posts( array( 'post_type' => 'event' ) );
while ( have_posts() ) { the_post(); the_title(); }
wp_reset_query(); // only ever needed to undo query_posts()

// RIGHT, for a block alongside the main content:
$events = new WP_Query( array( 'post_type' => 'event' ) );
while ( $events->have_posts() ) { $events->the_post(); the_title(); }
wp_reset_postdata();

// RIGHT, when you actually want to change what the page lists:
// hook pre_get_posts instead — see the pre_get_posts lesson.

Looping an array of posts by hand

This is the pattern for using template tags on an array that did not come from a WP_Query loop. setup_postdata() accepts a WP_Post, an object or, since WordPress 4.4, a post ID — but it never assigns the global itself, which is why the loop variable must be named $post.

Example · php
<?php
$featured = get_posts(
	array(
		'post_type'   => 'product',
		'numberposts' => 4,
		'meta_key'    => 'featured',
		'meta_value'  => '1',
	)
);

foreach ( $featured as $post ) {
	// setup_postdata() does NOT set the global $post — you assign it yourself
	// (done above by naming the foreach variable $post), then call it.
	setup_postdata( $post );
	?>
	<article>
		<h3><?php the_title(); ?></h3>
		<?php the_excerpt(); ?>
	</article>
	<?php
}
wp_reset_postdata();

Discussion

  • Be the first to comment on this lesson.