diff --git a/packages/data/README.md b/packages/data/README.md index 2e3e07a13ecc1b..d948bcc60046ca 100644 --- a/packages/data/README.md +++ b/packages/data/README.md @@ -387,7 +387,7 @@ _Parameters_ _Returns_ -- `WPDataRegistry`: Data registry. +- `DataRegistry`: Data registry. ### createRegistryControl @@ -820,8 +820,8 @@ function Paste( { children } ) { _Parameters_ -- _mapSelect_ `Function|StoreDescriptor|string`: Function called on every state change. The returned value is exposed to the component implementing this hook. The function receives the `registry.select` method on the first argument and the `registry` on the second argument. When a store key is passed, all selectors for the store will be returned. This is only meant for usage of these selectors in event callbacks, not for data needed to create the element tree. -- _deps_ `Array`: If provided, this memoizes the mapSelect so the same `mapSelect` is invoked on every state change unless the dependencies change. +- _mapSelect_ `SelectChooser`: Function called on every state change. The returned value is exposed to the component implementing this hook. The function receives the `registry.select` method on the first argument and the `registry` on the second argument. When a store key is passed, all selectors for the store will be returned. This is only meant for usage of these selectors in event callbacks, not for data needed to create the element tree. +- _deps_ `unknown[]`: If provided, this memoizes the mapSelect so the same `mapSelect` is invoked on every state change unless the dependencies change. _Returns_ diff --git a/packages/data/src/components/registry-provider/use-registry.js b/packages/data/src/components/registry-provider/use-registry.ts similarity index 94% rename from packages/data/src/components/registry-provider/use-registry.js rename to packages/data/src/components/registry-provider/use-registry.ts index 5a966907369359..c018ae0845123b 100644 --- a/packages/data/src/components/registry-provider/use-registry.js +++ b/packages/data/src/components/registry-provider/use-registry.ts @@ -45,7 +45,7 @@ import { Context } from './context'; * }; * ``` * - * @return {Function} A custom react hook exposing the registry context value. + * @return {Function} A custom react hook exposing the registry context value. */ export default function useRegistry() { return useContext( Context ); diff --git a/packages/data/src/components/use-select/index.js b/packages/data/src/components/use-select/index.ts similarity index 78% rename from packages/data/src/components/use-select/index.js rename to packages/data/src/components/use-select/index.ts index 2a4e9127c832d7..cfc5e34849d265 100644 --- a/packages/data/src/components/use-select/index.js +++ b/packages/data/src/components/use-select/index.ts @@ -17,31 +17,31 @@ import { useIsomorphicLayoutEffect } from '@wordpress/compose'; import useRegistry from '../registry-provider/use-registry'; import useAsyncMode from '../async-mode-provider/use-async-mode'; +import type { MapOf, SelectChooser, SelectMapper } from '../../types'; + const noop = () => {}; const renderQueue = createQueue(); -/** @typedef {import('../../types').StoreDescriptor} StoreDescriptor */ - /** * Custom react hook for retrieving props from registered selectors. * * In general, this custom React hook follows the * [rules of hooks](https://reactjs.org/docs/hooks-rules.html). * - * @param {Function|StoreDescriptor|string} mapSelect Function called on every state change. The - * returned value is exposed to the component - * implementing this hook. The function receives - * the `registry.select` method on the first - * argument and the `registry` on the second - * argument. - * When a store key is passed, all selectors for - * the store will be returned. This is only meant - * for usage of these selectors in event - * callbacks, not for data needed to create the - * element tree. - * @param {Array} deps If provided, this memoizes the mapSelect so the - * same `mapSelect` is invoked on every state - * change unless the dependencies change. + * @param mapSelect Function called on every state change. The + * returned value is exposed to the component + * implementing this hook. The function receives + * the `registry.select` method on the first + * argument and the `registry` on the second + * argument. + * When a store key is passed, all selectors for + * the store will be returned. This is only meant + * for usage of these selectors in event + * callbacks, not for data needed to create the + * element tree. + * @param deps If provided, this memoizes the mapSelect so the + * same `mapSelect` is invoked on every state + * change unless the dependencies change. * * @example * ```js @@ -87,9 +87,12 @@ const renderQueue = createQueue(); * } * ``` * - * @return {Function} A custom react hook. + * @return {Function} A custom react hook. */ -export default function useSelect( mapSelect, deps ) { +export default function useSelect( + mapSelect: SelectChooser, + deps?: unknown[] +): SelectChooser extends SelectMapper ? any : MapOf< Function > { const hasMappingFunction = 'function' === typeof mapSelect; // If we're recalling a store by its name or by @@ -106,7 +109,7 @@ export default function useSelect( mapSelect, deps ) { // `_mapSelect` if we can. const callbackMapper = useCallback( hasMappingFunction ? mapSelect : noop, - deps + deps as unknown[] ); const _mapSelect = hasMappingFunction ? callbackMapper : null; @@ -118,11 +121,11 @@ export default function useSelect( mapSelect, deps ) { const queueContext = useMemoOne( () => ( { queue: true } ), [ registry ] ); const [ , forceRender ] = useReducer( ( s ) => s + 1, 0 ); - const latestMapSelect = useRef(); + const latestMapSelect = useRef< SelectMapper >(); const latestIsAsync = useRef( isAsync ); - const latestMapOutput = useRef(); - const latestMapOutputError = useRef(); - const isMountedAndNotUnsubscribing = useRef(); + const latestMapOutput = useRef< unknown >(); + const latestMapOutputError = useRef< Error >(); + const isMountedAndNotUnsubscribing = useRef( false ); // Keep track of the stores being selected in the _mapSelect function, // and only subscribe to those stores later. @@ -141,7 +144,7 @@ export default function useSelect( mapSelect, deps ) { // in that case, we would still want to memoize it. const depsChangedFlag = useMemo( () => ( {} ), deps || [] ); - let mapOutput; + let mapOutput: unknown; if ( _mapSelect ) { mapOutput = latestMapOutput.current; @@ -154,7 +157,9 @@ export default function useSelect( mapSelect, deps ) { _mapSelect( registry.select, registry ) ); } catch ( error ) { - let errorMessage = `An error occurred while running 'mapSelect': ${ error.message }`; + let errorMessage = `An error occurred while running 'mapSelect': ${ + ( error as Error ).message + }`; if ( latestMapOutputError.current ) { errorMessage += `\nThe error may be correlated with this previous error:\n`; @@ -169,7 +174,7 @@ export default function useSelect( mapSelect, deps ) { } useIsomorphicLayoutEffect( () => { - if ( ! hasMappingFunction ) { + if ( ! _mapSelect ) { return; } @@ -189,15 +194,17 @@ export default function useSelect( mapSelect, deps ) { } ); useIsomorphicLayoutEffect( () => { - if ( ! hasMappingFunction ) { + if ( ! _mapSelect ) { return; } const onStoreChange = () => { - if ( isMountedAndNotUnsubscribing.current ) { + const currentMapSelect = latestMapSelect.current; + + if ( isMountedAndNotUnsubscribing.current && currentMapSelect ) { try { const newMapOutput = trapSelect( () => - latestMapSelect.current( registry.select, registry ) + currentMapSelect( registry.select, registry ) ); if ( @@ -207,7 +214,7 @@ export default function useSelect( mapSelect, deps ) { } latestMapOutput.current = newMapOutput; } catch ( error ) { - latestMapOutputError.current = error; + latestMapOutputError.current = error as Error; } forceRender(); } diff --git a/packages/data/src/registry.js b/packages/data/src/registry.js index f31cc1953828b6..efe530ee33f887 100644 --- a/packages/data/src/registry.js +++ b/packages/data/src/registry.js @@ -16,26 +16,7 @@ import coreDataStore from './store'; import { createEmitter } from './utils/emitter'; /** @typedef {import('./types').StoreDescriptor} StoreDescriptor */ - -/** - * @typedef {Object} WPDataRegistry An isolated orchestrator of store registrations. - * - * @property {Function} registerGenericStore Given a namespace key and settings - * object, registers a new generic - * store. - * @property {Function} registerStore Given a namespace key and settings - * object, registers a new namespace - * store. - * @property {Function} subscribe Given a function callback, invokes - * the callback on any change to state - * within any registered store. - * @property {Function} select Given a namespace key, returns an - * object of the store's registered - * selectors. - * @property {Function} dispatch Given a namespace key, returns an - * object of the store's registered - * action dispatchers. - */ +/** @typedef {import('./types').DataRegistry} DataRegistry */ /** * @typedef {Object} WPDataPlugin An object of registry function overrides. @@ -50,7 +31,7 @@ import { createEmitter } from './utils/emitter'; * @param {Object} storeConfigs Initial store configurations. * @param {Object?} parent Parent registry. * - * @return {WPDataRegistry} Data registry. + * @return {DataRegistry} Data registry. */ export function createRegistry( storeConfigs = {}, parent = null ) { const stores = {}; diff --git a/packages/data/src/types.ts b/packages/data/src/types.ts index 6094aa19674b55..1362d00e3715ae 100644 --- a/packages/data/src/types.ts +++ b/packages/data/src/types.ts @@ -1,4 +1,10 @@ -type MapOf< T > = { [ name: string ]: T }; +/** + * External dependencies + */ +// eslint-disable-next-line no-restricted-imports +import type { MutableRefObject } from 'react'; + +export type MapOf< T > = { [ name: string ]: T }; export type ActionCreator = Function | Generator; export type Resolver = Function | Generator; @@ -6,6 +12,15 @@ export type Selector = Function; export type AnyConfig = ReduxStoreConfig< any, any, any >; +export interface SelectMapper { + ( + select: ( reference: StoreReference ) => MapOf< Function >, + registry: DataRegistry + ): unknown; +} +export type SelectChooser = SelectMapper | StoreReference; +export type StoreReference = StoreDescriptor< any > | string; + export interface StoreInstance< Config extends AnyConfig > { getSelectors: () => SelectorsOf< Config >; getActions: () => ActionCreatorsOf< Config >; @@ -38,7 +53,17 @@ export interface ReduxStoreConfig< } export interface DataRegistry { + __experimentalMarkListeningStores: ( + callback: ( this: DataRegistry ) => unknown, + listeningStores: MutableRefObject< unknown > + ) => unknown; + __experimentalSubscribeStore: ( + storeName: string, + listener: () => void + ) => ReturnType< DataEmitter[ 'subscribe' ] >; + register: ( store: StoreDescriptor< any > ) => void; + select: ( chooser: SelectChooser, deps?: unknown[] ) => any; } export interface DataEmitter { diff --git a/packages/is-shallow-equal/src/index.js b/packages/is-shallow-equal/src/index.js index fddd62a022a3de..4970d8993b4a14 100644 --- a/packages/is-shallow-equal/src/index.js +++ b/packages/is-shallow-equal/src/index.js @@ -15,8 +15,8 @@ export { default as isShallowEqualArrays } from './arrays'; * Returns true if the two arrays or objects are shallow equal, or false * otherwise. * - * @param {any[]|ComparableObject} a First object or array to compare. - * @param {any[]|ComparableObject} b Second object or array to compare. + * @param {unknown[]|ComparableObject|any} a First object or array to compare. + * @param {unknown[]|ComparableObject|any} b Second object or array to compare. * * @return {boolean} Whether the two values are shallow equal. */