woocommerce/plugins/woocommerce-blocks/assets/js/base/context/hooks/collections/use-collection.ts

128 lines
4.1 KiB
TypeScript

/**
* External dependencies
*/
import { COLLECTIONS_STORE_KEY as storeKey } from '@woocommerce/block-data';
import { useSelect } from '@wordpress/data';
import { useRef } from '@wordpress/element';
import { useShallowEqual, useThrowError } from '@woocommerce/base-hooks';
import { isError } from '@woocommerce/types';
/**
* This is a custom hook that is wired up to the `wc/store/collections` data
* store. Given a collections option object, this will ensure a component is
* kept up to date with the collection matching that query in the store state.
*
* @throws {Object} Throws an exception object if there was a problem with the
* API request, to be picked up by BlockErrorBoundary.
*
* @param {Object} options An object declaring the various
* collection arguments.
* @param {string} options.namespace The namespace for the collection.
* Example: `'/wc/blocks'`
* @param {string} options.resourceName The name of the resource for the
* collection. Example:
* `'products/attributes'`
* @param {Array} [options.resourceValues] An array of values (in correct order)
* that are substituted in the route
* placeholders for the collection route.
* Example: `[10, 20]`
* @param {Object} [options.query] An object of key value pairs for the
* query to execute on the collection
* Example:
* `{ order: 'ASC', order_by: 'price' }`
* @param {boolean} [options.shouldSelect] If false, the previous results will be
* returned and internal selects will not
* fire.
*
* @return {Object} This hook will return an object with two properties:
* - results An array of collection items returned.
* - isLoading A boolean indicating whether the collection is
* loading (true) or not.
*/
export interface useCollectionOptions {
namespace: string;
resourceName: string;
resourceValues?: number[];
query?: Record< string, unknown >;
shouldSelect?: boolean;
isEditor?: boolean;
}
export const useCollection = < T >(
options: useCollectionOptions
): {
results: T[];
isLoading: boolean;
} => {
const {
namespace,
resourceName,
resourceValues = [],
query = {},
shouldSelect = true,
} = options;
if ( ! namespace || ! resourceName ) {
throw new Error(
'The options object must have valid values for the namespace and ' +
'the resource properties.'
);
}
const currentResults = useRef< { results: T[]; isLoading: boolean } >( {
results: [],
isLoading: true,
} );
// ensure we feed the previous reference if it's equivalent
const currentQuery = useShallowEqual( query );
const currentResourceValues = useShallowEqual( resourceValues );
const throwError = useThrowError();
const results = useSelect(
( select ) => {
if ( ! shouldSelect ) {
return null;
}
const store = select( storeKey );
const args = [
namespace,
resourceName,
currentQuery,
currentResourceValues,
];
const error = store.getCollectionError( ...args );
if ( error ) {
if ( isError( error ) ) {
throwError( error );
} else {
throw new Error(
'TypeError: `error` object is not an instance of Error constructor'
);
}
}
return {
results: store.getCollection< T[] >( ...args ),
isLoading: ! store.hasFinishedResolution(
'getCollection',
args
),
};
},
[
namespace,
resourceName,
currentResourceValues,
currentQuery,
shouldSelect,
throwError,
]
);
// if selector was not bailed, then update current results. Otherwise return
// previous results
if ( results !== null ) {
currentResults.current = results;
}
return currentResults.current;
};