HOC (Higher-Order Components)

Настало время рассмотреть со всех сторон механизм предназначенный для расширения функциональных возможностей компонента с помощью компонента-обертки обозначаемого как Higher-Order Components или сокращенно HOC.

Определение hoc

Раньше, при разработке React приложений разработчикам часто приходилось создавать конструкцию, известную в react сообществе, как HOC (Higher-Order Components).

HOC — это функция, которая на входе принимает один компонент, а на выходе возвращает новый с более расширенным функционалом. Другими словами, hoc — это функция, ожидающая в качестве параметров компонент (назовем его входным), который оборачивается в другой, объявленный в теле функции, компонент, выступающий в роли возвращаемого из функции значения (назовем его выходным). Слово “оборачивание”, применимое относительно компонентов, означает, что один компонент отрисовывает (рендерит) другой компонент, со всеми вытекающими из этого процесса (проксирования). За счет того, что входной компонент оборачивается в выходной, достигается расширение его и/или общего функционала. Кроме того, это позволяет устанавливать входному компоненту как зависимости, так и данные, полученные из внешних сервисов.

Определение hoc на основе функционального компонента

В качестве примера реализуем сценарий при котором пропсы компонента обертки определяемого в теле hoc разделяются на две категории. Первая необходима исключительно самому компоненту-обертке для генерации новых пропсов, которые в дальнейшем будут объединены с пропсами относящихся ко второй категории и установлены оборачиваемому компоненту.

Начать стоит с детального рассмотрения сигнатуры универсальной функции, ожидающей в качестве единственного параметра типа тип WrappedProps представляющий пропсы предназначенные исключительно оборачиваемому-компоненту ссылка на который доступна через единственный параметр WrappedComponent. WrappedComponent может принадлежать, как к функциональному FC<T>, так и классовому типу ComponentClass<T>, поэтому указываем ему в аннотации обобщенный тип Component<P> пропсы которого, помимо типа представленного аргументом типа WrappedProps, должны принадлежать ещё и к типу WrapperForWrappedProps описывающего значения создаваемые и устанавливаемые компонентом-оберткой.

Стоит упомянуть что, типComponent<P> является типом объединением представляющим классовые и функциональные React компоненты.

Поскольку в нашем конкретном примере функция hoc в качестве компонента-обертки определяет функциональный компонент, тип возвращаемого значения указан соответствующим образом FC<T>. Пропсы компонента-обертки должны принадлежать к нескольким типам одновременно поскольку для его работы требуются не только пропсы необходимые исключительно ему (WrapperProps), но и пропсы которые он лишь пробрасывает оборачиваемому-компоненту (WrappedProps). Поэтому аргумент типа представляющего возвращаемое значение является типом пересечение WrappedProps & WrapperProps.

ts
                /**[0] */
import React, {ComponentType} from "react";


/**[1] */
export interface WrapperProps {
    a: number;
    b: string;
}
/**[2] */
export interface WrapperForWrappedProps {
    c: boolean;
}

/**
 * [0] Импортируем обобщенный тип Component<P> представляющий
 * объединение классового и функционального React компонента.
 * [1] WrapperProps описывает данные необходимые
 * исключительно компоненту-обертке определяемому
 * внутри функции hoc, который генерирует
 * и устанавливает данные принадлежащие к типу 
 * WrapperForWrappedProps [2] обертываемому-компоненту.
 */


                /**[3]      [4] */
export function withHoc<WrappedProps>(
        /**[5]              [6]         [7]                 [8] */
    WrappedComponent: ComponentType<WrappedProps & WrapperForWrappedProps>)
       /**[9]    [10]           [11] */
        : FC<WrappedProps & WrapperProps>

/**
 * [3] определение универсальной функции hoc
 * чей единственный параметр типа WrappedProps [4]
 * представляет часть пропсов обертываемого-компонента, а их оставшаяся часть, генерируемая
 * компонентом-оберткой определенным в теле hoc, к типу WrapperForWrappedProps.
 * 
 * Единственный параметр hoc WrappedComponent [5] принадлежит
 * к обобщенному типу Component<P> [6], которому в качестве аргумента типа установлен
 * тип пересечение определяемый типами WrappedProps [7] и WrapperForWrappedProps [8].
 * 
 * Тип возвращаемого hoc значения обозначен как функциональный компонент [9] который по мимо пропсов
 * устанавливаемых разработчиком и прокидываемых компонентом-оберткой WrappedProps [10] ожидает ещё и пропсы
 * генерируемые и устанавливаемые компонентом-оберткой [11].
 * 
 * [!] принадлежность возращаемого hoc значения к функциональному типу указана лишь по причине того
 * что в нашем пример hoc возвращает именно его, а не классовый компонент. 
 */

Поскольку пример является минималистическим реализация тела hoc будет включать в себя лишь определение компонента-обертки выступающего в качестве возвращаемого значение. Тип компонента-обертки принадлежит к функциональному компоненту пропсы которого должны соответствовать типам описывающих как пропсы необходимые исключительно самому компоненту-обертке, так и оборачиваемому-компоненту. В теле компонента-обертки происходит разделение полученных пропсов на две части. Одна предназначается исключительно самому компоненту-обертке и служит для определения значений предназначенных для объединения со второй частью. Объединенные значения устанавливаются в качестве пропсов оборачиваемому-компоненту ссылка на который доступна через единственный параметр функции hoc. Стоит обратить внимание что поскольку вторая часть пропсов образуется как остаточные параметры полученные при деструктуризации, то их тип принадлежит к типу Pick<T, K>, который для совместимости с типом описывающим прокидываемые компонентом-оберткой пропсы необходимо сначала привести к типу unknown, а уже затем к конкретному типу WrappedProps.

ts
import React, {ComponentType} from "react";


export interface WrapperProps {
    a: number;
    b: string;
}
export interface WrapperForWrappedProps {
    c: boolean;
}

export function withHoc<WrappedProps>(
    WrappedComponent: ComponentType<WrappedProps & WrapperForWrappedProps>)
        : FC<WrappedProps & WrapperProps> {

                    /**[0]         [1]    [2]             [3] */
            const WrapperComponent:FC<WrappedProps & WrapperProps> = props => {
                  /**[4]            [5] */
                let {a, b, ...wrappedOnlyProps} = props;
                        /**[6] */
                let wrapperToWrappedProps = {
                    c: true
                };
                       /**[7]                      [8]                     [9]            [10]         [11] */
                let wrappedFullProps = {...wrapperToWrappedProps, ...wrappedOnlyProps as unknown as WrappedProps};

                            /**[12]               [13] */
                return <WrappedComponent {...wrappedFullProps} />
            }

                    /**[14] */
            return WrapperComponent;
}

/**
 * [0] определение комопнента-обертки принадлежащего
 * к типу функционального компонента [1] пропсы которого
 * одновременно принадлежат к типам описывающих пропсы предназначаемые
 * исключиетельно обертываемому-компоненту WrappedProps [2] и исключительно
 * компоненту-обертке WrapperProps [3]. В теле комопнента-обертки общие пропсы
 * разделяются с помощью механизма дествруктуризации на две категории, первая из
 * которых прдназначается самому компоненту-обертке [4], а вторая оборачиваемому-компоненту [5].
 * Поскольку пропсы пердназначенные оборачиваемому-компоненту [5] представляют из себя остаточные значения
 * полученные при деструктуризации, они принадлежат к типу Pick<T, K> что требует перед объединением их [9]
 * с пропсами созданными компонентом-оберткой [8] сначала к типу unknown [10], а затем уже к необходимому 
 * WrappedProps [11]. После этого слитые воедино пропсы можно устанавливать [13] компоненту [12] ссылка на который
 * доступна в качестве едлинственного параметра hoc.
 * 
 * [14] возвращаем из hoc компонент-обертку.
 */

Теперь необходимо определить компонент, пропсы которого будут принадледать к типам описывающих значения, чья установка разделена между разработчиком и компонентом-оберткой. Далее этот компонент необходимо передать в качестве аргумента созданному нами hoc чьё возвращаемое значение является комопнентом-оберткой с которым и будет взаимодействовать разработчик. Осталось создать экземпляр компонента-обертки и убедится как сила типизации позволяет установить в качестве пропсов только необходимые значения.

ts
/**[0] */
export interface CustomCompoenntProps {
    d: number;
    e: string;
}

                /**[1]                  [2]                     [3]                [4] [5][5] */
export const CustomComponent: FC<CustomCompoenntProps & WrapperForWrappedProps> = ({c, d, e}) => {
    return null;
} 

                    /**[6]              [7]         [8] */
export const CustomComponentWrapped = withHoc(CustomComponent);

/**
 * [0] объявление типа CustomCompoenntProps представляющего пропсы предназначенные
 * для обертываемого-компонента [1] и установка которых является
 * задачей разработчика. Пропсы компонента-обертки представленного
 * функциональным компонентом помимо типа CustomCompoenntProps [2]
 * описывающего значения устанавливаемые разработчиком [5]
 * также принадлежат к типу WrapperForWrappedProps [3] описывающего
 * значения устанавливаемые компонентом-оберткой [4].
 * 
 * Ссылка на оборачиваемый-компонент передается в качестве аргумента [8]
 * функции hoc [7], а результат вызова сохраняется в переменную представляющую
 * компонент-обертку [8].
 */


                     /**[9]   [9]    [10]  [10] */
<CustomComponentWrapped a={0} b={``} d={1} e={``} />; // Ok
                     /**[9]   [9]    [11]     [10]  [10] */
<CustomComponentWrapped a={0} b={``} c={true} d={1} e={``} />; // Error -> Property 'c' does not exist on type CustomCompoenntProps & WrapperProps'

/**
 * При создании экземпляра компонента-обертки будет необходимо установить
 * параметры описываемые как типом WrapperProps [9] и так и CustomCompoenntProps [10].
 * При попытке установить иные значения выозникнет ошибка. 
 */

Определение hoc на основе классового компонента

Поскольку пример для hoc, возвращающего в качестве компонента-обертки классовый компонент, отличается от предыдущего лишь заменой функции на класс и объявлением для него двух дополнительных типов *State и *Snapshot, в повторном комментировании происходящего попросту нет смысла.

ts
import React, {ComponentType, Component} from "react";


export interface WrapperProps {
    a: number;
    b: string;
}

/**[0] */
interface WrapperState {}
/**[1] */
interface WrapperSnapshot {}

export interface WrapperForWrappedProps {
    c: boolean;
}


/**
 * Поскольку комопнент-обертка будет представлен
 * классовым компонентом помимо описания его *Props
 * также появляется необходимость в объявлении типов
 * описывающих его *State [0] и *Snapshot [1].
 */

/**
 * [!] Стоит обратить внимание что по причине
 * упрощенности примера отсутствуют более компактные
 * псевдонимы для менее компактных типов.
 */

export function withHoc<WrappedProps>(
    WrappedComponent: ComponentType<WrappedProps & WrapperForWrappedProps>)
        : ComponentClass<WrappedProps & WrapperProps> {

                    /**[2] */
            class WrapperComponent extends Component<WrapperProps & WrappedProps, WrapperState, WrapperSnapshot> {
                render() {
                    let {a, b, ...wrappedOnlyProps} = this.props;
                    let wrapperToWrappedProps = {
                        c: true
                    };
                    let wrappedFullProps = {...wrapperToWrappedProps, ...wrappedOnlyProps as unknown as WrappedProps};


                    return <WrappedComponent {...wrappedFullProps} />
                }
            }


            return WrapperComponent;
}

/**
 * [2] определение комопнента-обертки в виде классового компонента.
 */

export interface CustomCompoenntProps {
    d: number;
    e: string;
}

export const CustomComponent: FC<CustomCompoenntProps & WrapperForWrappedProps> = ({c, d, e}) => {
    return null;
} 

export const CustomComponentWrapped = withHoc(CustomComponent);


<CustomComponentWrapped a={0} b={``} d={1} e={``} />; // Ok
<CustomComponentWrapped a={0} b={``} c={true} d={1} e={``} />; // Error -> Property 'c' does not exist on type CustomCompoenntProps & WrapperProps'