Is temporarily overwriting $wp_query a bad idea?

In general, I agree with Howdy_McGee that one should avoid overwriting the main query unless absolutely necessary – more often than not, modifying the main query with something like a 'pre_get_posts' hook is a better solution to such scenarios.

Manually overwriting the global $wp_query can cause all sorts of unintended behaviors if you are not exceedingly careful, including breaking pagination, among other things. It is, in essence, an even more simplistic equivalent to the often criticized query_posts() function, which the WordPress Code Reference notes:

This function will completely override the main query and isn’t intended for use by plugins or themes. Its overly-simplistic approach to modifying the main query can be problematic and should be avoided wherever possible. In most cases, there are better, more performant options for modifying the main query such as via the ‘pre_get_posts’ action within WP_Query.

In this instance however, where you require either:

  • Simultaneous access to both the main query as well as a secondary query for their distinct content and/or conditional tags
  • A generic implementation in something like a reusable template part that can be applied to either the main query or any custom query

then one solution is to store the “contextual” WP_Query instance in a local variable, and invoke WP_Query‘s conditional methods on that variable instead of using their globally-available conditional tag function counterparts (which always explicitly reference the main query, global $wp_query).

For instance, if you wanted your “contextual query variable” to refer to a custom secondary query in single and archive templates for your custom post type, my-custom-post-type, but refer to the main query in every other case, you could do the following:

Theme’s functions.php file, or a plugin file:

function wpse_232115_get_contextual_query() {
  global $wp_query;
  static $contextual_query;

  // A convenient means to check numerous conditionals without a bunch of '||' operations,
  // i.e "if( $i_need_a_custom_query )"
  if( in_array( true,
    [
      is_singular( 'my-custom-post-type' ),
      is_post_type_archive( 'my-custom-post-type' )
    ]
  ) ) {
    // Create the contextual query instance, if it doesn't yet exist
    if( ! isset( $contextual_query ) ) {
      $query_args = [
        //...
      ];

      $contextual_query = new WP_Query( $query_args );
    }

    return $contextual_query;
  }

  return $wp_query;
}

“Generic” template-part files:

$query = wpse_232115_get_contextual_query();

// Contextual Query loop (loops through your custom query when 'my-custom-post-type's
// are being displayed, the main $wp_query otherwise.
if ( $query->have_posts() ) : while ( $query->have_posts() ) : $query->the_post();
  // Tags dependent on The Loop will refer to the contextual query if The Loop
  // was set up with the "$query->" prefix, as above. Without it, they always
  // refer to the main query.
  the_title();

  // The global conditional tags always refer to the main query
  if( is_singular() ) {
    //... Do stuff is the main query result is_singular();
  }

  // Conditional methods on the $query object reference describe either
  // the main query, or a custom query depending on the context.
  if( $query->is_archive() ) {
    //... Do stuff if the $query query result is an archive
  }
// End the loop
endwhile; endif;

I’m not sure if this is the best solution, but it’s how I would think to approach the problem.