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';