Skip to content

Commit

Permalink
Improves the types of createHigherOrderComponent and its usages (#41138)
Browse files Browse the repository at this point in the history
* createHOC: migrate default export to named

* Evolve the types of createHigherOrderComponent and its usages

* Fix index.native.js
  • Loading branch information
jsnajdr authored Jun 3, 2022
1 parent a4c3b14 commit 488c9de
Show file tree
Hide file tree
Showing 12 changed files with 121 additions and 104 deletions.
4 changes: 2 additions & 2 deletions packages/compose/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ name, returns the enhanced component augmented with a generated displayName.

_Parameters_

- _mapComponent_ `HigherOrderComponent< HOCProps >`: Function mapping component to enhanced component.
- _mapComponent_ `( Inner: TInner ) => TOuter`: Function mapping component to enhanced component.
- _modifierName_ `string`: Seed name from which to generated display name.

_Returns_
Expand All @@ -105,7 +105,7 @@ const ConditionalComponent = ifCondition(

_Parameters_

- _predicate_ `( props: TProps ) => boolean`: Function to test condition.
- _predicate_ `( props: Props ) => boolean`: Function to test condition.

_Returns_

Expand Down
18 changes: 11 additions & 7 deletions packages/compose/src/higher-order/if-condition/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
/**
* External dependencies
*/
import type { ComponentType } from 'react';

/**
* Internal dependencies
*/
import createHigherOrderComponent from '../../utils/create-higher-order-component';
import { createHigherOrderComponent } from '../../utils/create-higher-order-component';

/**
* Higher-order component creator, creating a new component which renders if
Expand All @@ -20,18 +25,17 @@ import createHigherOrderComponent from '../../utils/create-higher-order-componen
*
* @return Higher-order component.
*/
const ifCondition = < TProps extends Record< string, any > >(
predicate: ( props: TProps ) => boolean
) =>
createHigherOrderComponent< {} >(
( WrappedComponent ) => ( props ) => {
if ( ! predicate( props as TProps ) ) {
function ifCondition< Props >( predicate: ( props: Props ) => boolean ) {
return createHigherOrderComponent(
( WrappedComponent: ComponentType< Props > ) => ( props: Props ) => {
if ( ! predicate( props ) ) {
return null;
}

return <WrappedComponent { ...props } />;
},
'ifCondition'
);
}

export default ifCondition;
60 changes: 29 additions & 31 deletions packages/compose/src/higher-order/pure/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/**
* External dependencies
*/
import type { ComponentType, ComponentClass } from 'react';

/**
* WordPress dependencies
*/
Expand All @@ -7,43 +12,36 @@ import { Component } from '@wordpress/element';
/**
* Internal dependencies
*/
import createHigherOrderComponent from '../../utils/create-higher-order-component';

/**
* External dependencies
*/
import type { ComponentType, ComponentClass } from 'react';
import { createHigherOrderComponent } from '../../utils/create-higher-order-component';

/**
* Given a component returns the enhanced component augmented with a component
* only re-rendering when its props/state change
*/
const pure = createHigherOrderComponent(
< TProps extends Record< string, any > >(
Wrapped: ComponentType< TProps >
) => {
if ( Wrapped.prototype instanceof Component ) {
return class extends ( Wrapped as ComponentClass< TProps > ) {
shouldComponentUpdate( nextProps: TProps, nextState: any ) {
return (
! isShallowEqual( nextProps, this.props ) ||
! isShallowEqual( nextState, this.state )
);
}
};
}

return class extends Component< TProps > {
shouldComponentUpdate( nextProps: TProps ) {
return ! isShallowEqual( nextProps, this.props );
}

render() {
return <Wrapped { ...this.props } />;
const pure = createHigherOrderComponent( function < Props >(
WrappedComponent: ComponentType< Props >
): ComponentType< Props > {
if ( WrappedComponent.prototype instanceof Component ) {
return class extends ( WrappedComponent as ComponentClass< Props > ) {
shouldComponentUpdate( nextProps: Props, nextState: any ) {
return (
! isShallowEqual( nextProps, this.props ) ||
! isShallowEqual( nextState, this.state )
);
}
};
},
'pure'
);
}

return class extends Component< Props > {
shouldComponentUpdate( nextProps: Props ) {
return ! isShallowEqual( nextProps, this.props );
}

render() {
return <WrappedComponent { ...this.props } />;
}
};
},
'pure' );

export default pure;
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import deprecated from '@wordpress/deprecated';
/**
* Internal dependencies
*/
import createHigherOrderComponent from '../../utils/create-higher-order-component';
import { createHigherOrderComponent } from '../../utils/create-higher-order-component';
import Listener from './listener';

/**
Expand Down
29 changes: 19 additions & 10 deletions packages/compose/src/higher-order/with-instance-id/index.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,30 @@
/**
* Internal dependencies
*/
import createHigherOrderComponent from '../../utils/create-higher-order-component';
import {
createHigherOrderComponent,
WithInjectedProps,
WithoutInjectedProps,
} from '../../utils/create-higher-order-component';
import useInstanceId from '../../hooks/use-instance-id';

type InstanceIdProps = { instanceId: string | number };

/**
* A Higher Order Component used to be provide a unique instance ID by
* component.
*/
const withInstanceId = createHigherOrderComponent< {
instanceId: string | number;
} >( ( WrappedComponent ) => {
return ( props ) => {
const instanceId = useInstanceId( WrappedComponent );
// @ts-ignore
return <WrappedComponent { ...props } instanceId={ instanceId } />;
};
}, 'withInstanceId' );
const withInstanceId = createHigherOrderComponent(
< C extends WithInjectedProps< C, InstanceIdProps > >(
WrappedComponent: C
) => {
return ( props: WithoutInjectedProps< C, InstanceIdProps > ) => {
const instanceId = useInstanceId( WrappedComponent );
// @ts-ignore
return <WrappedComponent { ...props } instanceId={ instanceId } />;
};
},
'instanceId'
);

export default withInstanceId;
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Internal dependencies
*/
import createHigherOrderComponent from '../../utils/create-higher-order-component';
import { createHigherOrderComponent } from '../../utils/create-higher-order-component';
import usePreferredColorScheme from '../../hooks/use-preferred-color-scheme';

/**
Expand Down
41 changes: 22 additions & 19 deletions packages/compose/src/higher-order/with-safe-timeout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
* External dependencies
*/
import { without } from 'lodash';
import type { ComponentType } from 'react';

/**
* WordPress dependencies
Expand All @@ -12,7 +11,11 @@ import { Component } from '@wordpress/element';
/**
* Internal dependencies
*/
import createHigherOrderComponent from '../../utils/create-higher-order-component';
import {
createHigherOrderComponent,
WithInjectedProps,
WithoutInjectedProps,
} from '../../utils/create-higher-order-component';

/**
* We cannot use the `Window['setTimeout']` and `Window['clearTimeout']`
Expand All @@ -22,25 +25,24 @@ import createHigherOrderComponent from '../../utils/create-higher-order-componen
* In the case of this component, we only handle the simplest case where
* `setTimeout` only accepts a function (not a string) and an optional delay.
*/
type TimeoutProps = {
interface TimeoutProps {
setTimeout: ( fn: () => void, delay: number ) => number;
clearTimeout: ( id: number ) => void;
};
}

/**
* A higher-order component used to provide and manage delayed function calls
* that ought to be bound to a component's lifecycle.
*/
const withSafeTimeout = createHigherOrderComponent< TimeoutProps >(
< TProps extends TimeoutProps >(
OriginalComponent: ComponentType< TProps >
const withSafeTimeout = createHigherOrderComponent(
< C extends WithInjectedProps< C, TimeoutProps > >(
OriginalComponent: C
) => {
return class WrappedComponent extends Component<
Omit< TProps, keyof TimeoutProps >
> {
type WrappedProps = WithoutInjectedProps< C, TimeoutProps >;
return class WrappedComponent extends Component< WrappedProps > {
timeouts: number[];

constructor( props: Omit< TProps, keyof TimeoutProps > ) {
constructor( props: WrappedProps ) {
super( props );
this.timeouts = [];
this.setTimeout = this.setTimeout.bind( this );
Expand All @@ -51,7 +53,7 @@ const withSafeTimeout = createHigherOrderComponent< TimeoutProps >(
this.timeouts.forEach( clearTimeout );
}

setTimeout( fn: ( ...args: any[] ) => void, delay: number ) {
setTimeout( fn: () => void, delay: number ) {
const id = setTimeout( () => {
fn();
this.clearTimeout( id );
Expand All @@ -66,13 +68,14 @@ const withSafeTimeout = createHigherOrderComponent< TimeoutProps >(
}

render() {
const props = {
...this.props,
setTimeout: this.setTimeout,
clearTimeout: this.clearTimeout,
} as TProps;

return <OriginalComponent { ...props } />;
return (
// @ts-ignore
<OriginalComponent
{ ...this.props }
setTimeout={ this.setTimeout }
clearTimeout={ this.clearTimeout }
/>
);
}
};
},
Expand Down
2 changes: 1 addition & 1 deletion packages/compose/src/higher-order/with-state/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import deprecated from '@wordpress/deprecated';
/**
* Internal dependencies
*/
import createHigherOrderComponent from '../../utils/create-higher-order-component';
import { createHigherOrderComponent } from '../../utils/create-higher-order-component';

/**
* A Higher Order Component used to provide and manage internal component state
Expand Down
4 changes: 2 additions & 2 deletions packages/compose/src/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Utils.
export { default as createHigherOrderComponent } from './utils/create-higher-order-component';
// The `createHigherOrderComponent` helper and helper types.
export * from './utils/create-higher-order-component';

// Compose helper (aliased flowRight from Lodash)
export { default as compose } from './higher-order/compose';
Expand Down
4 changes: 2 additions & 2 deletions packages/compose/src/index.native.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Utils.
export { default as createHigherOrderComponent } from './utils/create-higher-order-component';
// The `createHigherOrderComponent` helper and helper types.
export * from './utils/create-higher-order-component';

// Compose helper (aliased flowRight from Lodash)
export { default as compose } from './higher-order/compose';
Expand Down
57 changes: 30 additions & 27 deletions packages/compose/src/utils/create-higher-order-component/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,13 @@
import { camelCase, upperFirst } from 'lodash';
import type { ComponentType } from 'react';

/**
* Higher order components can cause props to be obviated. For example a HOC that
* injects i18n props will obviate the need for the i18n props to be passed to the component.
*
* If a HOC does not obviate the need for any specific props then we default to `{}` which
* essentially subtracts 0 from the original props of the passed in component. An example
* of this is the `pure` HOC which does not change the API surface of the component but
* simply modifies the internals.
*/
export type HigherOrderComponent< HOCProps extends Record< string, any > > = <
InnerProps extends HOCProps
>(
Inner: ComponentType< InnerProps >
) => {} extends HOCProps
? ComponentType< InnerProps >
: ComponentType< Omit< InnerProps, keyof HOCProps > >;
type GetProps< C > = C extends ComponentType< infer P > ? P : never;

export type WithoutInjectedProps< C, I > = Omit< GetProps< C >, keyof I >;

export type WithInjectedProps< C, I > = ComponentType<
WithoutInjectedProps< C, I > & I
>;

/**
* Given a function mapping a component to an enhanced component and modifier
Expand All @@ -30,19 +21,31 @@ export type HigherOrderComponent< HOCProps extends Record< string, any > > = <
*
* @return Component class with generated display name assigned.
*/
function createHigherOrderComponent<
HOCProps extends Record< string, any > = {}
>( mapComponent: HigherOrderComponent< HOCProps >, modifierName: string ) {
return < InnerProps extends HOCProps >(
Inner: ComponentType< InnerProps >
) => {
export function createHigherOrderComponent<
TInner extends ComponentType< any >,
TOuter extends ComponentType< any >
>( mapComponent: ( Inner: TInner ) => TOuter, modifierName: string ) {
return ( Inner: TInner ) => {
const Outer = mapComponent( Inner );
const displayName = Inner.displayName || Inner.name || 'Component';
Outer.displayName = `${ upperFirst(
camelCase( modifierName )
) }(${ displayName })`;
Outer.displayName = hocName( modifierName, Inner );
return Outer;
};
}

export default createHigherOrderComponent;
/**
* Returns a displayName for a higher-order component, given a wrapper name.
*
* @example
* hocName( 'MyMemo', Widget ) === 'MyMemo(Widget)';
* hocName( 'MyMemo', <div /> ) === 'MyMemo(Component)';
*
* @param name Name assigned to higher-order component's wrapper component.
* @param Inner Wrapped component inside higher-order component.
* @return Wrapped name of higher-order component.
*/
const hocName = ( name: string, Inner: ComponentType< any > ) => {
const inner = Inner.displayName || Inner.name || 'Component';
const outer = upperFirst( camelCase( name ) );

return `${ outer }(${ inner })`;
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Component } from '@wordpress/element';
/**
* Internal dependencies
*/
import createHigherOrderComponent from '../';
import { createHigherOrderComponent } from '../';

describe( 'createHigherOrderComponent', () => {
it( 'should use default name for anonymous function', () => {
Expand Down

0 comments on commit 488c9de

Please sign in to comment.