В этой статье предполагается, что у вас есть базовые знания о JavaScript и React.

  1. Реакция: Higher Order Component(HOC) – это функция, которая принимает реакцию КОМПОНЕНТ как param и возвращает КОМПОНЕНТ.
  2. JavaScript: Higher Order Function(HOF) – это функция, которая принимает ФУНКЦИЮ в качестве параметра, ИЛИ возвращает ФУНКЦИЮ.
export function withThemeContext(ComponentToWrap: React.FC): React.FC {
 
    return function Wrapper(props): JSX.Element {
      const [themeType, setThemeType] = useState<ThemeType>('light');
      return (
            <ThemeContext.Provider value={{ themeType, setThemeType}}>
                    <ComponentToWrap {...props} />
            </ThemeContext.Provider>
        );
    };
}

В этом фрагменте кода упаковано довольно много концепций, мы постараемся их разобрать.

Итак, чтобы понять React HOC, вам нужно хорошо разбираться в функциях высшего порядка JavaScript, потому что HOC — это всего лишь разновидность HOF. Я рекомендую прочитать эту статью, чтобы понять, что компоненты React Function по сути являются функциями JavaScript.

withThemeContext : это функция, которая принимает Функция( React.FC) в качестве параметра и возвращает другую Функция<. /em>(который называется Wrapper типа React.FC).

Функция, возвращаемая из withThemeContext, также является функцией. Эта функция оборачивает входной компонент с помощью ThemeContext, чтобы мы могли получить доступ к теме внутри компонента и его дочерних компонентов. Эта статья не о том, как работает контекст React, если вы не знакомы с ним, просто представьте, что этот withThemeContext HOC предоставляет некоторую функцию компоненту Wrapped.

Применение

function MyComponent() {
    // my component logic
}

const MyComponentWithThemeContext = withThemeContext(MyComponent);
// Now MyComponentWithThemeContext with the new Wrapper component returned from
// withThemeContext function

// app.tsx
function App(): JSX.Element {
  return <MyComponentWithThemeContext />
}

// index.ts
ReactDOM.render(<App/>, document.getElementById('app'));

Вы можете спросить, почему бы нам не добавить логику оболочки в сам MyComponent, это возможно. Однако представьте, что в вашем приложении слишком много такой логики, и обработка их в одном компоненте очень быстро усложняется. При таком подходе у HOC есть только одна цель, в этом конкретном примере withThemeContext несет только одну ответственность за управление темой, которая придерживается принципа дизайна Single Responsibility.

Чтобы лучше понять важность этого шаблона, давайте предположим, что вы хотите добавить ErrorBoundary в дерево компонентов для обработки любых необработанных исключений в вашем приложении. Что бы вы сделали? Если вы измените какой-либо из этих компонентов, НЕТ. Вот где HOC становится удобным, иногда вы можете добавить больше функций в свое приложение, даже не касаясь ни одного из существующих компонентов, React Error Boundary — один из хороших примеров. У меня есть статья на эту тему здесь, если интересно.

export function withErrorBoundary(ComponentToWrap: React.FC): React.FC {
 
    return function Wrapper(props): JSX.Element {

      return <ErrorBoundary>
                 <ComponentToWrap {...props} />
            </ErrorBoundary>;
    };
}

const CompWithThemeContext = withThemeContext(MyComponent);
const CompWithErrorBoundary = withErrorBoundary(ComponentWithThemeContext);

// did you notice how the output of the first hoc passed to the other hoc
// this mean we are able to compose independent features together to build up application logic
// DECOMPOSITION then COMPOSITION = FLEXIBILITY + REUSABILITY + MANAGABILITY

// let's say now you have one more hoc, 
// just implement it independently and follow the same pattern. like so

const CompWithAnotherFeature = withAnotherFeature(MyComponent);
const CompWithThemeContext = withThemeContext(CompWithAnotherFeature);
const CompWithErrorBoundary = withErrorBoundary(ComponentWithThemeContext);
// the above three lines are equivalent to the following
const Component = withErrorBoundary(withThemeContext(withAnotherFeature(MyComponent)));

Собираем все вместе

// types.ts
export type ThemeType =  'dark' | 'light';

export interface IThemeContext {
    themeType: ThemeType;
    setThemeType: (themeType: ThemeType) => void;
}
// themeContext.ts 

import { IThemeContext, ThemeType } from './types';
import React from 'react';

// create a reat context of type IThemeContext
const ThemeContext = React.createContext<IThemeContext>({} as IThemeContext);
// withThemeContext.tsx

export function withThemeContext(ComponentToWrap: React.FC): React.FC {
 
    return function Wrapper(props): JSX.Element {
      const [themeType, setThemeType] = useState<ThemeType>('light');
      return (
            <ThemeContext.Provider value={{ themeType, setThemeType}}>
                    <ComponentToWrap {...props} />
            </ThemeContext.Provider>
        );
    };
}
import React, { useContext } from 'react';
import { ThemeContext } from './themeContext';

// This button provide the user to choose a theme
function ChangeThemeButton() {
    const { setThemeType } = useContext(ThemeContext);

    const onDarkClick = useCallback(() => { setThemeType('dark') }, []);
    const onLightClick = useCallback(() => { setThemeType('light') }, []);

    return (
      <div>
          <button onClick={onLightClick}>Change to light</button>
          <button onClick={onDarkClick}>Change to dark</button>
      </div>
    );
}
// withErrorBoundary.ts
export function withErrorBoundary(ComponentToWrap: React.FC): React.FC {
 
    return function Wrapper(props): JSX.Element {
   
      return <ErrorBoundary>  // please check my other article for ErrorBoundary
                 <ComponentToWrap {...props} />
            </ErrorBoundary>;
    };
}
// myComposer.ts

const CompWithAnotherFeature = withAnotherFeature(MyComponent);
const CompWithThemeContext = withThemeContext(CompWithAnotherFeature);
const CompWithErrorBoundary = withErrorBoundary(ComponentWithThemeContext);

export { CompWithErrorBoundary as MyComponent }; // just renaming before exporting
// app.tsx
import React from 'react';
import { MyComponent } from './myComposer'; // please note this is the renamed component

export function App() {

  return <MyComponent />
}
// index.ts
import ReactDOM from 'react-dom';

ReactDOM.render(<App/>, document.getElementById('app'));

Заключение

В этой статье мы обсуждали в основном HOC, однако, чтобы сделать сценарий более практичным, я использовал некоторые другие концепции, с которыми вы, возможно, не знакомы. то есть React Context API, хуки React, которые будут обсуждаться на других сессиях. А еще есть интересная тема про HOC vs Custom hooks. В большинстве случаев Custom Hooks могут заменить HOC гораздо лучше, но не полностью.

Далее: React Custom Hooks и HOC