diff --git a/packages/core-data/src/hooks/test/use-entity-record.js b/packages/core-data/src/hooks/test/use-entity-record.js
index e8d3834aab5c55..e735fe6e6739c1 100644
--- a/packages/core-data/src/hooks/test/use-entity-record.js
+++ b/packages/core-data/src/hooks/test/use-entity-record.js
@@ -33,11 +33,11 @@ describe( 'useEntityRecord', () => {
const TEST_RECORD = { id: 1, hello: 'world' };
- it( 'retrieves the relevant entity record', async () => {
+ it( 'resolves the entity record when missing from the state', async () => {
+ // Provide response
+ triggerFetch.mockImplementation( () => TEST_RECORD );
+
let data;
- await registry
- .dispatch( coreDataStore )
- .receiveEntityRecords( 'root', 'widget', [ TEST_RECORD ] );
const TestComponent = () => {
data = useEntityRecord( 'root', 'widget', 1 );
return
;
@@ -47,34 +47,14 @@ describe( 'useEntityRecord', () => {
);
+
expect( data ).toEqual( {
- record: TEST_RECORD,
+ records: undefined,
hasResolved: false,
isResolving: false,
status: 'IDLE',
} );
- // Required to make sure no updates happen outside of act()
- await act( async () => {
- jest.advanceTimersByTime( 1 );
- } );
- } );
-
- it( 'resolves the entity if missing from state', async () => {
- // Provide response
- triggerFetch.mockImplementation( () => TEST_RECORD );
-
- let data;
- const TestComponent = () => {
- data = useEntityRecord( 'root', 'widget', 1 );
- return ;
- };
- render(
-
-
-
- );
-
await act( async () => {
jest.advanceTimersByTime( 1 );
} );
diff --git a/packages/core-data/src/hooks/test/use-entity-records.js b/packages/core-data/src/hooks/test/use-entity-records.js
new file mode 100644
index 00000000000000..e85418360a36bb
--- /dev/null
+++ b/packages/core-data/src/hooks/test/use-entity-records.js
@@ -0,0 +1,78 @@
+/**
+ * WordPress dependencies
+ */
+import triggerFetch from '@wordpress/api-fetch';
+import { createRegistry, RegistryProvider } from '@wordpress/data';
+
+jest.mock( '@wordpress/api-fetch' );
+
+/**
+ * External dependencies
+ */
+import { act, render } from '@testing-library/react';
+
+/**
+ * Internal dependencies
+ */
+import { store as coreDataStore } from '../../index';
+import useEntityRecords from '../use-entity-records';
+
+describe( 'useEntityRecords', () => {
+ let registry;
+ beforeEach( () => {
+ jest.useFakeTimers();
+
+ registry = createRegistry();
+ registry.register( coreDataStore );
+ } );
+
+ afterEach( () => {
+ jest.runOnlyPendingTimers();
+ jest.useRealTimers();
+ } );
+
+ const TEST_RECORDS = [
+ { id: 1, hello: 'world1' },
+ { id: 2, hello: 'world2' },
+ { id: 3, hello: 'world3' },
+ ];
+
+ it( 'resolves the entity records when missing from the state', async () => {
+ // Provide response
+ triggerFetch.mockImplementation( () => TEST_RECORDS );
+
+ let data;
+ const TestComponent = () => {
+ data = useEntityRecords( 'root', 'widget', { status: 'draft' } );
+ return ;
+ };
+ render(
+
+
+
+ );
+
+ expect( data ).toEqual( {
+ records: null,
+ hasResolved: false,
+ isResolving: false,
+ status: 'IDLE',
+ } );
+
+ await act( async () => {
+ jest.advanceTimersByTime( 1 );
+ } );
+
+ // Fetch request should have been issued
+ expect( triggerFetch ).toHaveBeenCalledWith( {
+ path: '/wp/v2/widgets?context=edit&status=draft',
+ } );
+
+ expect( data ).toEqual( {
+ records: TEST_RECORDS,
+ hasResolved: true,
+ isResolving: false,
+ status: 'SUCCESS',
+ } );
+ } );
+} );
diff --git a/packages/core-data/src/hooks/test/use-query-select.js b/packages/core-data/src/hooks/test/use-query-select.js
index ea69f971214248..a8c15ae9e0bc59 100644
--- a/packages/core-data/src/hooks/test/use-query-select.js
+++ b/packages/core-data/src/hooks/test/use-query-select.js
@@ -123,8 +123,8 @@ describe( 'useQuerySelect', () => {
expect( querySelectData ).toEqual( {
data: 'bar',
isResolving: false,
- hasStarted: false,
hasResolved: false,
+ status: 'IDLE',
} );
} );
@@ -171,8 +171,8 @@ describe( 'useQuerySelect', () => {
expect( querySelectData ).toEqual( {
data: 10,
isResolving: false,
- hasStarted: false,
hasResolved: false,
+ status: 'IDLE',
} );
await act( async () => {
@@ -188,8 +188,8 @@ describe( 'useQuerySelect', () => {
expect( querySelectData ).toEqual( {
data: 15,
isResolving: false,
- hasStarted: true,
hasResolved: true,
+ status: 'SUCCESS',
} );
} );
} );
diff --git a/packages/core-data/src/hooks/use-entity-record.ts b/packages/core-data/src/hooks/use-entity-record.ts
index 79931bccf2e2c7..2cbce8c34c574b 100644
--- a/packages/core-data/src/hooks/use-entity-record.ts
+++ b/packages/core-data/src/hooks/use-entity-record.ts
@@ -49,7 +49,7 @@ interface EntityRecordResolution< RecordType > {
* ```
*
* In the above example, when `PageTitleDisplay` is rendered into an
- * application, the price and the resolution details will be retrieved from
+ * application, the page and the resolution details will be retrieved from
* the store state using `getEntityRecord()`, or resolved if missing.
*
* @return {EntityRecordResolution} Entity record data.
@@ -60,28 +60,13 @@ export default function __experimentalUseEntityRecord< RecordType >(
name: string,
recordId: string | number
): EntityRecordResolution< RecordType > {
- const { data, isResolving, hasResolved } = useQuerySelect(
+ const { data: record, ...rest } = useQuerySelect(
( query ) => query( coreStore ).getEntityRecord( kind, name, recordId ),
[ kind, name, recordId ]
);
- let status;
- if ( isResolving ) {
- status = Status.Resolving;
- } else if ( hasResolved ) {
- if ( data ) {
- status = Status.Success;
- } else {
- status = Status.Error;
- }
- } else {
- status = Status.Idle;
- }
-
return {
- status,
- record: data,
- isResolving,
- hasResolved,
+ record,
+ ...rest,
};
}
diff --git a/packages/core-data/src/hooks/use-entity-records.ts b/packages/core-data/src/hooks/use-entity-records.ts
new file mode 100644
index 00000000000000..29e7fd2a9a3feb
--- /dev/null
+++ b/packages/core-data/src/hooks/use-entity-records.ts
@@ -0,0 +1,79 @@
+/**
+ * Internal dependencies
+ */
+import useQuerySelect from './use-query-select';
+import { store as coreStore } from '../';
+import { Status } from './constants';
+
+interface EntityRecordsResolution< RecordType > {
+ /** The requested entity record */
+ records: RecordType[] | null;
+
+ /**
+ * Is the record still being resolved?
+ */
+ isResolving: boolean;
+
+ /**
+ * Is the record resolved by now?
+ */
+ hasResolved: boolean;
+
+ /** Resolution status */
+ status: Status;
+}
+
+/**
+ * Resolves the specified entity records.
+ *
+ * @param kind Kind of the requested entities.
+ * @param name Name of the requested entities.
+ * @param queryArgs HTTP query for the requested entities.
+ *
+ * @example
+ * ```js
+ * import { useEntityRecord } from '@wordpress/core-data';
+ *
+ * function PageTitlesList() {
+ * const { records, isResolving } = useEntityRecords( 'postType', 'page' );
+ *
+ * if ( isResolving ) {
+ * return 'Loading...';
+ * }
+ *
+ * return (
+ *
+ * {records.map(( page ) => (
+ * - { page.title }
+ * ))}
+ *
+ * );
+ * }
+ *
+ * // Rendered in the application:
+ * //
+ * ```
+ *
+ * In the above example, when `PageTitlesList` is rendered into an
+ * application, the list of records and the resolution details will be retrieved from
+ * the store state using `getEntityRecords()`, or resolved if missing.
+ *
+ * @return {EntityRecordsResolution} Entity records data.
+ * @template RecordType
+ */
+export default function __experimentalUseEntityRecords< RecordType >(
+ kind: string,
+ name: string,
+ queryArgs: unknown = {}
+): EntityRecordsResolution< RecordType > {
+ const { data: records, ...rest } = useQuerySelect(
+ ( query ) =>
+ query( coreStore ).getEntityRecords( kind, name, queryArgs ),
+ [ kind, name, queryArgs ]
+ );
+
+ return {
+ records,
+ ...rest,
+ };
+}
diff --git a/packages/core-data/src/hooks/use-query-select.ts b/packages/core-data/src/hooks/use-query-select.ts
index fc89c98ed78b93..4de52a28d8ba15 100644
--- a/packages/core-data/src/hooks/use-query-select.ts
+++ b/packages/core-data/src/hooks/use-query-select.ts
@@ -7,6 +7,7 @@ import { useSelect } from '@wordpress/data';
* Internal dependencies
*/
import memoize from './memoize';
+import { Status } from './constants';
export const META_SELECTORS = [
'getIsResolving',
@@ -97,19 +98,31 @@ const enrichSelectors = memoize( ( selectors ) => {
}
Object.defineProperty( resolvers, selectorName, {
get: () => ( ...args ) => {
- const {
- getIsResolving,
- hasStartedResolution,
- hasFinishedResolution,
- } = selectors;
+ const { getIsResolving, hasFinishedResolution } = selectors;
const isResolving = !! getIsResolving( selectorName, args );
+ const hasResolved =
+ ! isResolving &&
+ hasFinishedResolution( selectorName, args );
+ const data = selectors[ selectorName ]( ...args );
+
+ let status;
+ if ( isResolving ) {
+ status = Status.Resolving;
+ } else if ( hasResolved ) {
+ if ( data ) {
+ status = Status.Success;
+ } else {
+ status = Status.Error;
+ }
+ } else {
+ status = Status.Idle;
+ }
+
return {
- data: selectors[ selectorName ]( ...args ),
+ data,
+ status,
isResolving,
- hasStarted: hasStartedResolution( selectorName, args ),
- hasResolved:
- ! isResolving &&
- hasFinishedResolution( selectorName, args ),
+ hasResolved,
};
},
} );
diff --git a/packages/core-data/src/index.js b/packages/core-data/src/index.js
index 9000df9286acf6..214587375e4ecf 100644
--- a/packages/core-data/src/index.js
+++ b/packages/core-data/src/index.js
@@ -75,5 +75,6 @@ register( store );
export { default as EntityProvider } from './entity-provider';
export { default as __experimentalUseEntityRecord } from './hooks/use-entity-record';
+export { default as __experimentalUseEntityRecords } from './hooks/use-entity-records';
export * from './entity-provider';
export * from './fetch';