В этой статье мы рассмотрим компоненты высшего порядка (Higher-Order Components, HOC) в React.

Что такое HOC?

HOC — это особая техника в React, при которой функция принимает аргумент Component и возвращает новый компонент.

function composeComponent(Component) {
    return class extends React.Component {
        render() {
            return <Component />
        }
    }

}

В данном примере функция composeComponent принимает аргумент Component и возвращает компонент ES6 class. Возвращенный класс отображает аргумент Component. Аргумент Component является компонентом React, который будет отображаться возвращенным компонентом class.

Например:

class CatComponent {
    render() {
        return <div>Cat Component</div>
    }
}

CatComponent отображает следующее:

Cat Component

CatComponet может быть передан функции composeComponent для получения другого компонента:

const composedCatComponent = composeComponent(CatComponent)

composedCatComponent может быть отображен:

<composedCatComponent /> 

Результат выглядит следующим образом:

Cat Component

Та же функция высшего порядка, как и в JS.

Функция высшего порядка

Функция высшего порядка — это шаблон в JS, при котором одна функция принимает другую функцию и возвращает функцию в качестве результата, что является возможным благодаря композиционному характеру JS. Это означает, что:

  • объекты
  • массивы
  • строки
  • числа
  • логические значения
  • функции

могут быть переданы функциям в качестве аргументов или возвращены из функции.

function mul(x) {
    return (y) => {
        return x * y
    }
}
const mulTwo = mul(2)

mulTwo(2) // 4
mulTwo(3) // 9
mulTwo(4) // 8
mulTwo(5) // 10

Функция mul возвращает функцию, которая захватывает x в замыкании. x теперь доступен для возвращенной функции, а mul является теперь функцией высшего порядка, поскольку она возвращает функцию. Это означает, что ее можно использовать для создания более специфических функций с использованием различных аргументов.

Например, ее можно использовать для создания функции, которая утраивает свои аргументы:

function mul(x) {
    return (y) => {
        return x * y
    }
}
const triple = mul(3)

triple(2) // 6
triple(3) // 9
triple(4) // 12
triple(5) // 15

В чем преимущества элементов высшего порядка? При постоянном повторении логики нужно найти способ разместить логику в одном месте и использовать ее оттуда. Функции высшего порядка предоставляют шаблон, который можно использовать для этой реализации.

В приведенном выше примере при непрерывном умножении на 3 в приложении можно создать функцию, которая возвращает функцию, умножающую на 3 triple. Поэтому при необходимости написать код multiplication by 3, нужно просто вызвать triple, передающий число для умножения на 3 в качестве параметра.

Общие проблемы при работе с HOC

В чем преимущества использования HOC в приложении React?

В процессе написания программы одна и та же логика может повторяться снова и снова.

Например, приложение для просмотра и редактирования документов. Приложение будет аутентифицировано, поэтому только аутентифицированные пользователи смогут получить доступ к панели инструментов, редактировать, просматривать или удалять документы. Маршруты будут выглядеть следующим образом:

<Route path="/" component={App}>
    <Route path="/dashboard" component={Documents}/>
    <Route path="document/:id/view" component={ViewDocument} />
    <Route path="documents/:id/delete" component={DelDocument} />
    <Route path="documents/:id/edit" component={EditDocument}/>
</Route>

Документы нужно аутентифицировать, чтобы только аутентифицированные пользователи могли получить к ним доступ. Для этого необходимо сделать следующее:

class Doucments extends React.Component {
    componentwillMount() {
        if(!this.props.isAuth){
            this.context.router.push("/")
        }
    }
    componentWillUpdate(nextProps) {
        if(!nextProps.isAuth) {
            this.context.router.push("/")            
        }
    }
    render() {
        return <div>Documents Paegs!!!</div>
    }
}

function mapstateToProps(state) {
    isAuth: state.auth
}
export default connect(mapStateToProps)(Documents)

State.auth содержит состояние пользователя. Если пользователь не аутентифицирован, то значение будет false, в обратно случае  —  true. Функция connect отобразит состояние для объектов props isAuth. Затем при подключении компонента к DOM запускается componentWillMount. Здесь нужно проверить, обладает ли prop isAuth значением true. При значении true метод передается вниз, а при значении false метод передает маршрут “/” индексной страницы маршрутизатору. В результате браузер загружает страницу индекса внутри рендеринга компонента documents, запрещая не аутентифицированным пользователям получить доступ.

Когда компонент выполняет ре-рендеринг после первоначального рендеринга, необходимо выполнить те же действия в componentWillUpdate, чтобы проверить, авторизован ли пользователь. Если нет, загружается страница индекса.

Теперь сделаем то же самое в ViewDocument:

class ViewDoucment extends React.Component {
    componentwillMount() {
        if(!this.props.isAuth){
            this.context.router.push("/")
        }
    }
    componentWillUpdate(nextProps) {
        if(!nextProps.isAuth) {
            this.context.router.push("/")            
        }
    }
    render() {
        return <div>View Document Page!!!</div>
    }
}

function mapstateToProps(state) {
    isAuth: state.auth
}
export default connect(mapStateToProps)(ViewDocument)

И в EditDocument:

class EditDocument extends React.Component {
    componentwillMount() {
        if(!this.props.isAuth){
            this.context.router.push("/")
        }
    }
    componentWillUpdate(nextProps) {
        if(!nextProps.isAuth) {
            this.context.router.push("/")            
        }
    }
    render() {
        return <div>Edit Document Page!!!</div>
    }
}

function mapstateToProps(state) {
    isAuth: state.auth
}
export default connect(mapStateToProps)(EditDocument)

Также в DelDocument:

class DelDocument extends React.Component {
    componentwillMount() {
        if(!this.props.isAuth){
            this.context.router.push("/")
        }
    }
    componentWillUpdate(nextProps) {
        if(!nextProps.isAuth) {
            this.context.router.push("/")            
        }
    }
    render() {
        return <div>Delete Document Page!!!</div>
    }
}

function mapstateToProps(state) {
    isAuth: state.auth
}
export default connect(mapStateToProps)(DelDocument)

Страницы выполняют различные действия, однако реализация одинакова.

Каждый компонент:

  • подключается к состоянию с помощью соединения react-redux;
  • отображает состояние auth для свойств props isAuth;
  • проверяет аутентификацию в componentWillMount;
  • проверяет аутентификацию в componentWillUpdate.

Представьте, что в проекте появится больше компонентов, в каждом из которых нужно выполнить те же действия. Это займет много времени.

Поэтому нужно найти способ для определения логики в одном месте. Лучше всего использовать HOC.

Для этого перемещаем всю логику в функцию, которая будет возвращать компонент:

function requireAuthentication(composedComponent) {
    class Authentication extends React.Component {
        componentwillMount() {
            if(!this.props.isAuth){
                this.context.router.push("/")
            }
        }
        componentWillUpdate(nextProps) {
            if(!nextProps.isAuth) {
                this.context.router.push("/")            
            }
        }
        render() {
            <ComposedComponent />
        }
    }
    function mapstateToProps(state) {
        isAuth: state.auth
    }
    return connect(mapStateToProps)(Authentication)
}

Все реализации находятся внутри компонента Authentication. Функция requireAuthentication соединяет компонент Authentication с хранилищем и возвращает его. Затем Authentication отображает компонент, переданный через аргумент ComposedCompoennt.

Маршруты теперь выглядят следующим образом:

<Route path="/" component={App}>
    <Route path="/dashboard" component={requireAuthentication(Documents)}/>
    <Route path="document/:id/view" component={requireAuthentication(ViewDocument)} />
    <Route path="documents/:id/delete" component={requireAuthentication(DelDocument)} />
    <Route path="documents/:id/edit" component={requireAuthentication(EditDocument)}/>
</Route>

Таким образом, независимо от количества маршрутов в приложении, нужно просто вызвать функцию requireAuthentication с переданным ей компонентом.

Заключение

Функция высшего порядка:

  • возвращает функцию;
  • используется для решения проблемы DRY.

Компонент высшего порядка:

  • принимает компонент в качестве аргумента;
  • возвращает другой компонент;
  • возвращенный компонент отображает оригинальный компонент;
  • используется для решения проблемы DRY.

Спасибо за внимание !!!

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Chidume Nnamdi ????: Understanding Higher-Order Components in React

Предыдущая статьяУвеличиваем производительность приложения React + Redux с библиотекой Reselect
Следующая статья4 golang-сниппета, которые вводят в заблуждение разработчиков C#!