Як описано в документації React, компонент вищого порядку (HOC) це функція, що приймає компонент в ролі аргументу й повертає наново створений компонент. Компонент, що повертається, як правило, доповнюється особливостями, наданими компонентом вищого порядку. Сам же компонент вищого порядку не є частиною програмного забезпечення, яку можна взяти й встановити. Це техніка, яка може бути корисною в написанні повторно використовуваного та підтримуваного коду.
Крок 1: Налаштування компонентів
Штучний застосунок, створений для цілей цієї статті, складається з двох компонентів: CommentsList
та BlogPost
. Вони обидва відображені всередині App
, головного компоненту застосунку.
# App.vue
<template>
<div id="app">
<blog-post/>
<comments-list/>
</div>
</template>
<script>
import CommentsList from './components/CommentsList'
import BlogPost from './components/BlogPost'
export default {
name: 'app',
components: {
'blog-post': BlogPost,
'comments-list': CommentsList
}
}
</script>
Компонент CommentsList
відображає список коментарів, отриманих із зовнішнього джерела даних. Крім того, на хук mounted
додається прослуховувач подій, який відстежує зміни у джерелі даних та відповідно оновлює список коментарів. На хуку beforeDestroy
прослуховувач видаляється.
# components/CommentsList.vue
<template>
<ul>
<li
v-for="(comment, index) in comments"
:key="index"
>{{comment}}</li>
</ul>
</template>
<script>
import DataSource from '../store/source.js'
export default {
name: 'comments-list',
data() {
return {
comments: DataSource.getComments()
}
},
methods: {
handleChange() {
this.comments = DataSource.getComments()
}
},
mounted() {
DataSource.addChangeListener(this.handleChange)
},
beforeDestroy() {
DataSource.removeChangeListener(this.handleChange)
}
}
</script>
Компонент BlogPost
відображає вміст посту блогу. Так само, як і CommentsList
, він бере дані зі зовнішнього джерела й оновлює вміст посту при кожній зміні в цьому джерелі.
# components/BlogPost.vue
<template>
<div>
{{blogPost}}
</div>
</template>
<script>
import DataSource from '../store/source.js'
export default {
data() {
return {
blogPost: DataSource.getBlogPost()
}
},
methods: {
handleChange() {
this.blogPost = DataSource.getBlogPost()
}
},
mounted() {
DataSource.addChangeListener(this.handleChange)
},
beforeDestroy() {
DataSource.removeChangeListener(this.handleChange)
}
}
</script>
Компоненти BlogPost
та CommentsList
поділяють чотири функціональності:
- Беруть дані зі зовнішнього джерела даних (у цьому випадку з
DataSource
) всередині хукуmounted
. - Оновлюють
data
при кожному оновлені в зовнішньому джерелі даних. - Додають прослуховувач змін до джерела даних.
- Прибирають прослуховувач змін із джерела даних.
Щоб уникнути повторень коду, загальна логіка між BlogPost
та CommentsList
може бути витягнута у компонент вищого порядку.
Крок 2: Компонент вищого порядку
На цьому етапі я переміщу дубльований код у компонент вищого порядку, який називається withSubscription
.
Компонент вищого порядку це функція, що бере компонент в ролі аргументу й повертає новий компонент. Напишімо його на Vue:
# hocs/withSubscription.js
import Vue from 'vue'
import CommentsList from '~/components/CommentsList.vue'
const withSubscription = (component) => {
return Vue.component('withSubscription', {
render(createElement) {
return createElement(component)
}
}
}
const CommentsListWithSubscription = withSubscription(CommentsList)
На даному етапі компонент вищого порядку робить небагато. Він просто бере компонент й створює новий, який рендерить прийнятий компонент.
Наступний крок: реалізувати в ньому загальну логіку. Потрібно додати хуки mounted
й beforeDestroy
, та метод handleChange
, який буде викликатися при кожному оновлені.
# hocs/withSubscription.js
import DataSource from '../store/source'
import Vue from 'vue'
const withSubscription = (component) => {
return Vue.component('withSubscription', {
render(createElement) {
return createElement(component)
},
methods: {
handleChange() {
}
},
mounted() {
DataSource.addChangeListener(this.handleChange)
},
beforeDestroy() {
DataSource.removeChangeListener(this.handleChange)
}
})
}
export default withSubscription
Тепер новий компонент, що повертається за допомогою компонента вищого порядку, має необхідні хуки життєвого циклу. Метод handleChange
залишається порожнім. Обидва компоненти містять метод handleChange
, однак він має дещо різну реалізацію в кожному з них.
Компонент вищого порядку може приймати більше одного аргументу. Зараз withSubscription
в ролі аргументу приймає тільки компонент. Щоб викликати кастомну логіку всередині handleChange
, потрібен другий аргумент. Ним буде метод, що має викликатися при кожній зміні джерела даних. Метод, що передається, повертає оновлені дані, які повинні бути передані наново створеному компоненту в ролі вхідного параметра.
# hocs/withSubscription.js
import DataSource from '../store/source'
import Vue from 'vue'
const withSubscription = (component, selectData) => {
return Vue.component('withSubscription', {
render(createElement, context) {
return createElement(component, {
props: {
content: this.fetchedData
}
})
},
data() {
return {
fetchedData: null
}
},
methods: {
handleChange() {
this.fetchedData = selectData(DataSource)
}
},
mounted() {
DataSource.addChangeListener(this.handleChange)
},
beforeDestroy() {
DataSource.removeChangeListener(this.handleChange)
}
})
}
export default withSubscription
Використання компоненту вищого порядку всередині App.vue
виглядає наступним чином:
# App.vue
<template>
<div id="app">
<blog-post/>
<comments-list/>
</div>
</template>
<script>
import CommentsList from './components/CommentsList'
import BlogPost from './components/BlogPost'
import withSubscription from './hocs/withSubscription'
const BlogPostWithSubscription = withSubscription(BlogPost, (DataSource) => {
return DataSource.getBlogPost()
})
const CommentsListWithSubscription = withSubscription(CommentsList, (DataSource) => DataSource.getComments())
export default {
name: 'app',
components: {
'blog-post': BlogPostWithSubscription,
'comments-list': CommentsListWithSubscription
}
}
</script>
А тут код BlogPost
та CommentsList
:
# components/BlogPost.vue
<template>
<div>
{{content}}
</div>
</template>
<script>
export default {
props: ['content']
}
</script>
----
# components/CommentsList.vue
<template>
<ul>
<li v-for="(comment, index) in content" :key="index">{{comment}}</li>
</ul>
</template>
<script>
export default {
name: 'comments-list',
props: ['content']
}
</script>
Все це виглядає дуже добре, але є одна відсутня частина. Що, якщо мені потрібно передати ID у BlogPost
? Або, що, якщо я хочу згенерувати подію з BlogPost
у компонент App
? З поточною реалізацією це не спрацює.
Крок 3: Обробка вхідних параметрів та подій у компоненті вищого порядку
По-перше, трохи змінимо реалізацію методу getBlogPost
у DataSource
. Щоб дізнатися, який пост був отриманий та повернутий, в ролі другого аргументу потрібно взяти id посту. Оскільки фактичний виклик getBlogPost
відбувається всередині компоненту BlogPost
, має сенс передавати в ролі вхідного параметра бажаний id посту блогу й використовувати його при виклику методу getBlogPost
. Для цього мені потрібно зробити дві речі: перенести вхідний параметр id
з компонента App
у компонент BlogPost
та змінити функцію, яку я передаю у компонент вищого порядку, так, щоб вона приймала другий аргумент — вхідні параметри, які вона має передати далі у BlogPost
.
# App.vue
<template>
<div id="app">
<blog-post :id="1"/>
</div>
</template>
<script>
import BlogPost from './components/BlogPost'
import withSubscription from './hocs/withSubscription'
const BlogPostWithSubscription = withSubscription(BlogPost, (DataSource, props) => {
return DataSource.getBlogPost(props.id)
})
export default {
name: 'app',
components: {
'blog-post': BlogPostWithSubscription
}
}
</script>
---
# components/BlogPost.vue
<template>
<div>
{{content}}
</div>
</template>
<script>
export default {
props: ['content', 'id']
}
</script>
Тепер мені треба оновити компонент вищого порядку, щоб він знав, як передавати вхідні параметри у компонент, який він відображає.
# hocs/withSubscription.js
import DataSource from '../store/source'
import Vue from 'vue'
const withSubscription = (component, selectData) => {
const originalProps = component.props || [];
return Vue.component('withSubscription', {
render(createElement) {
return createElement(component, {
props: {
...originalProps,
content: this.fetchedData
}
})
},
props: [...originalProps],
data() {
return {
fetchedData: null
}
},
methods: {
handleChange() {
this.fetchedData = selectData(DataSource, this.$props)
}
},
mounted() {
DataSource.addChangeListener(this.handleChange)
},
beforeDestroy() {
DataSource.removeChangeListener(this.handleChange)
}
})
}
export default withSubscription
Перше, що додається у компонент вищого порядку — читання оригінальних вхідних параметрів з компоненту, який він відображає. Ці параметри зберігаються в константі originalProps
. У Vue компонент має визначити, які вхідні параметри він приймає. withSubscription
повинна приймати ті самі вхідні параметри, що і компонент, який вона відображає, щоб мати можливість пізніше передати їх в нього. Це робиться за допомогою наступного рядка коду:
return Vue.component('withSubscription', {
...
props: [...originalProps], # <= цей рядок
...
}
Остання частина, що була оновлена, — виклик функції selectData
всередині методу handleChange
. Був доданий другий аргумент — вхідні параметри компоненту вищого порядку — this.$props
. Властивість $props
є властивістю екземпляру компоненту Vue, доступної з версії Vue 2.2.
Я охопив передачу вхідних параметрів у дочірній компонент. Останньою відсутньою частиною є генерація подій від дочірнього компонента до предка.
Додамо прослуховувач подій в компонент App.vue
, та щось, що генеруватиме подію в BlogPost.vue
.
# App.vue
<template>
<div id="app">
<blog-post :id="1" @click="onClick"/>
</div>
</template>
---
# components/BlogPost.vue
<template>
<div>
<button @click="$emit('click', 'aloha')">CLICK ME!</button>
{{data}}
</div>
</template>
<script>
export default {
props: ['data', 'id']
}
</script>
Слід пам'ятати, що BlogPost
не виноситься всередину App
, бо є посередник — компонент вищого порядку withSubscription
.
Щоб передати прослуховувачі подій у компонент, який виноситься, мені потрібно додати один рядок коду в компонент вищого порядку.
# hocs/withSubscription.js
return Vue.component('withSubscription', {
...
on: {...this.$listeners} # <= цей рядок,
})
Точно так само, як і у this.$props
, тут є властивість екземпляра $listener
, яка містить прослуховувачі подій v-on
області видимості предка.
Повний застосунок зі статті можна знайти за посиланням: https://github.com/bognix/vue-hoc
Ще немає коментарів