Prevent multiple collection data requests using debounce (https://github.com/woocommerce/woocommerce-blocks/pull/1233)

* Add context for collection data query

* Introduce useCollectionData hook

* Implement hook in filter blocks

* Update API to handle nuances of counts instead of client side

* Clone requests so original is untouched

* Prevent dupe requests is working

* Cleanup

* Update assets/js/base/hooks/use-collection-data.js

Co-Authored-By: Albert Juhé Lluveras <aljullu@gmail.com>

* Update assets/js/base/hooks/use-collection-data.js

Co-Authored-By: Albert Juhé Lluveras <aljullu@gmail.com>

* Feedback

* Remove context
This commit is contained in:
Mike Jolley 2019-11-23 17:04:30 +00:00 committed by GitHub
parent c595444b7f
commit c55a387657
8 changed files with 226 additions and 62 deletions

View File

@ -3,4 +3,5 @@ export * from './use-shallow-equal';
export * from './use-store-products';
export * from './use-collection';
export * from './use-collection-header';
export * from './use-collection-data';
export * from './use-previous';

View File

@ -0,0 +1,124 @@
/**
* External dependencies
*/
import { useState, useEffect, useMemo } from '@wordpress/element';
import {
useQueryStateByContext,
useQueryStateByKey,
useCollection,
} from '@woocommerce/base-hooks';
import { useQueryStateContext } from '@woocommerce/base-context/query-state-context';
import { useDebounce } from 'use-debounce';
import { sortBy } from 'lodash';
/**
* Internal dependencies
*/
import { useShallowEqual } from './use-shallow-equal';
const buildCollectionDataQuery = ( collectionDataQueryState ) => {
const query = collectionDataQueryState;
if ( collectionDataQueryState.calculate_attribute_counts ) {
query.calculate_attribute_counts = sortBy(
collectionDataQueryState.calculate_attribute_counts.map(
( { taxonomy, queryType } ) => {
return {
taxonomy,
query_type: queryType,
};
}
),
[ 'taxonomy', 'query_type' ]
);
}
return query;
};
export const useCollectionData = ( {
queryAttribute,
queryPrices,
queryState,
} ) => {
let context = useQueryStateContext();
context = `${ context }-collection-data`;
const [ collectionDataQueryState ] = useQueryStateByContext( context );
const [
calculateAttributesQueryState,
setCalculateAttributesQueryState,
] = useQueryStateByKey( 'calculate_attribute_counts', [], context );
const [
calculatePriceRangeQueryState,
setCalculatePriceRangeQueryState,
] = useQueryStateByKey( 'calculate_price_range', null, context );
const currentQueryAttribute = useShallowEqual( queryAttribute || {} );
const currentQueryPrices = useShallowEqual( queryPrices );
useEffect( () => {
if (
typeof currentQueryAttribute === 'object' &&
Object.keys( currentQueryAttribute ).length
) {
const foundAttribute = calculateAttributesQueryState.find(
( attribute ) => {
return (
attribute.taxonomy === currentQueryAttribute.taxonomy
);
}
);
if ( ! foundAttribute ) {
setCalculateAttributesQueryState( [
...calculateAttributesQueryState,
currentQueryAttribute,
] );
}
}
}, [
currentQueryAttribute,
calculateAttributesQueryState,
setCalculateAttributesQueryState,
] );
useEffect( () => {
if (
calculatePriceRangeQueryState !== currentQueryPrices &&
currentQueryPrices !== undefined
) {
setCalculatePriceRangeQueryState( currentQueryPrices );
}
}, [
currentQueryPrices,
setCalculatePriceRangeQueryState,
calculatePriceRangeQueryState,
] );
// Defer the select query so all collection-data query vars can be gathered.
const [ shouldSelect, setShouldSelect ] = useState( false );
const [ debouncedShouldSelect ] = useDebounce( shouldSelect, 200 );
if ( ! shouldSelect ) {
setShouldSelect( true );
}
const collectionDataQueryVars = useMemo( () => {
return buildCollectionDataQuery( collectionDataQueryState );
}, [ collectionDataQueryState ] );
return useCollection( {
namespace: '/wc/store',
resourceName: 'products/collection-data',
query: {
...queryState,
page: undefined,
per_page: undefined,
orderby: undefined,
order: undefined,
...collectionDataQueryVars,
},
shouldSelect: debouncedShouldSelect,
} );
};

View File

@ -5,6 +5,7 @@ import {
useCollection,
useQueryStateByKey,
useQueryStateByContext,
useCollectionData,
} from '@woocommerce/base-hooks';
import {
useCallback,
@ -91,32 +92,6 @@ const AttributeFilterBlock = ( {
.flatMap( ( attribute ) => attribute.slug );
}, [ productAttributesQuery, attributeObject ] );
const filteredCountsQueryState = useMemo( () => {
// If doing an "AND" query, we need to remove current taxonomy query so counts are not affected.
const modifiedQueryState =
blockAttributes.queryType === 'or'
? productAttributesQuery.filter(
( item ) => item.attribute !== attributeObject.taxonomy
)
: productAttributesQuery;
// Take current query and remove paging args.
return {
...queryState,
orderby: undefined,
order: undefined,
per_page: undefined,
page: undefined,
attributes: modifiedQueryState,
calculate_attribute_counts: [ attributeObject.taxonomy ],
};
}, [
queryState,
attributeObject,
blockAttributes,
productAttributesQuery,
] );
const {
results: attributeTerms,
isLoading: attributeTermsLoading,
@ -130,11 +105,12 @@ const AttributeFilterBlock = ( {
const {
results: filteredCounts,
isLoading: filteredCountsLoading,
} = useCollection( {
namespace: '/wc/store',
resourceName: 'products/collection-data',
query: filteredCountsQueryState,
shouldSelect: blockAttributes.attributeId > 0,
} = useCollectionData( {
queryAttribute: {
taxonomy: attributeObject.taxonomy,
queryType: blockAttributes.queryType,
},
queryState,
} );
/**

View File

@ -2,9 +2,9 @@
* External dependencies
*/
import {
useCollection,
useQueryStateByKey,
useQueryStateByContext,
useCollectionData,
} from '@woocommerce/base-hooks';
import { Fragment, useCallback, useState, useEffect } from '@wordpress/element';
import PriceSlider from '@woocommerce/base-components/price-slider';
@ -22,19 +22,9 @@ const PriceFilterBlock = ( { attributes, isPreview = false } ) => {
'max_price'
);
const [ queryState ] = useQueryStateByContext();
const { results, isLoading } = useCollection( {
namespace: '/wc/store',
resourceName: 'products/collection-data',
query: {
...queryState,
min_price: undefined,
max_price: undefined,
orderby: undefined,
order: undefined,
per_page: undefined,
page: undefined,
calculate_price_range: true,
},
const { results, isLoading } = useCollectionData( {
queryPrices: true,
queryState,
} );
const [ minPrice, setMinPrice ] = useState();

View File

@ -143,27 +143,71 @@ class ProductCollectionData extends RestContoller {
$filters = new ProductQueryFilters();
if ( ! empty( $request['calculate_price_range'] ) ) {
$price_results = $filters->get_filtered_price( $request );
$filter_request = clone $request;
$filter_request->set_param( 'min_price', null );
$filter_request->set_param( 'max_price', null );
$price_results = $filters->get_filtered_price( $filter_request );
$return['min_price'] = $price_results->min_price;
$return['max_price'] = $price_results->max_price;
}
if ( ! empty( $request['calculate_attribute_counts'] ) ) {
$return['attribute_counts'] = [];
$counts = $filters->get_attribute_counts( $request, $request['calculate_attribute_counts'] );
$taxonomy__or_queries = [];
$taxonomy__and_queries = [];
foreach ( $counts as $key => $value ) {
$return['attribute_counts'][] = [
'term' => $key,
'count' => $value,
];
foreach ( $request['calculate_attribute_counts'] as $attributes_to_count ) {
if ( 'or' === $attributes_to_count['query_type'] ) {
$taxonomy__or_queries[] = $attributes_to_count['taxonomy'];
} else {
$taxonomy__and_queries[] = $attributes_to_count['taxonomy'];
}
}
$return['attribute_counts'] = [];
// Or type queries need special handling because the attribute, if set, needs removing from the query first otherwise counts would not be correct.
if ( $taxonomy__or_queries ) {
foreach ( $taxonomy__or_queries as $taxonomy ) {
$filter_request = clone $request;
$filter_attributes = $filter_request->get_param( 'attributes' );
if ( ! empty( $filter_attributes ) ) {
$filter_attributes = array_filter(
$filter_attributes,
function( $query ) {
return $query['attribute'] !== $taxonomy;
}
);
}
$filter_request->set_param( 'attributes', $filter_attributes );
$counts = $filters->get_attribute_counts( $filter_request, [ $taxonomy ] );
foreach ( $counts as $key => $value ) {
$return['attribute_counts'][] = [
'term' => $key,
'count' => $value,
];
}
}
}
if ( $taxonomy__and_queries ) {
$counts = $filters->get_attribute_counts( $request, $taxonomy__and_queries );
foreach ( $counts as $key => $value ) {
$return['attribute_counts'][] = [
'term' => $key,
'count' => $value,
];
}
}
}
if ( ! empty( $request['calculate_rating_counts'] ) ) {
$return['rating_counts'] = [];
$counts = $filters->get_rating_counts( $request );
$return['rating_counts'] = [];
foreach ( $counts as $key => $value ) {
$return['rating_counts'][] = [
@ -194,7 +238,22 @@ class ProductCollectionData extends RestContoller {
'description' => __( 'If requested, calculates attribute term counts for products in the collection.', 'woo-gutenberg-products-block' ),
'type' => 'array',
'items' => array(
'type' => 'string',
'type' => 'object',
'properties' => array(
'taxonomy' => array(
'description' => __( 'Taxonomy name.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'query_type' => array(
'description' => __( 'Query type being performed which may affect counts. Valid values include "and" and "or".', 'woo-gutenberg-products-block' ),
'type' => 'string',
'enum' => array( 'and', 'or' ),
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
),
'default' => array(),
);

View File

@ -100,15 +100,15 @@ This endpoint allows you to get aggregate data from a collection of products, fo
```http
GET /products/collection-data
GET /products/collection-data?calculate_price_range=true
GET /products/collection-data?calculate_attribute_counts=pa_size,pa_color
GET /products/collection-data?calculate_attribute_counts[0][query_type]=or&calculate_attribute_counts[0][taxonomy]=pa_color
GET /products/collection-data?calculate_rating_counts=true
```
| Attribute | Type | Required | Description |
| :--------------------------- | :----- | :------: | :--------------------------------------------------------------------------------------------------------------------------------------- |
| `calculate_price_range` | bool | No | Returns the min and max price for the product collection. If false, only `null` will be returned. |
| `calculate_attribute_counts` | string | No | Returns attribute counts for a list of attribute (taxonomy) names you pass in via the parameter. If empty, only `null` will be returned. |
| `calculate_rating_counts` | bool | No | Returns the counts of products with a certain average rating, 1-5. If false, only `null` will be returned. |
| Attribute | Type | Required | Description |
| :--------------------------- | :----- | :------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `calculate_price_range` | bool | No | Returns the min and max price for the product collection. If false, only `null` will be returned. |
| `calculate_attribute_counts` | object | No | Returns attribute counts for a list of attribute taxonomies you pass in via this parameter. Each should be provided as an object with keys "taxonomy" and "query_type". If empty, `null` will be returned. |
| `calculate_rating_counts` | bool | No | Returns the counts of products with a certain average rating, 1-5. If false, only `null` will be returned. |
**In addition to the above attributes**, all product list attributes are supported. This allows you to get data for a certain subset of products. See [the products API list products section](#list-products) for the full list.

View File

@ -64,6 +64,12 @@ class ProductQueryFilters {
public function get_attribute_counts( $request, $attributes = [] ) {
global $wpdb;
// Remove paging and sorting params from the request.
$request->set_param( 'page', null );
$request->set_param( 'per_page', null );
$request->set_param( 'order', null );
$request->set_param( 'orderby', null );
// Grab the request from the WP Query object, and remove SQL_CALC_FOUND_ROWS and Limits so we get a list of all products.
$product_query = new ProductQuery();

View File

@ -107,7 +107,15 @@ class ProductCollectionData extends TestCase {
ProductHelper::create_variation_product();
$request = new WP_REST_Request( 'GET', '/wc/store/products/collection-data' );
$request->set_param( 'calculate_attribute_counts', 'pa_size' );
$request->set_param(
'calculate_attribute_counts',
[
[
'taxonomy' => 'pa_size',
'query_type' => 'and',
]
]
);
$response = $this->server->dispatch( $request );
$data = $response->get_data();