Это вторая часть серии о компонентах высшего порядка (HOCs). Сегодня я расскажу о различных шаблонах компонентов высшего порядка, которые полезны и реализуемы. С помощью HOC вы можете абстрагировать избыточный код в слой более высокого порядка. Однако, как и любые другие шаблоны, привыкание к HOC займет некоторое время. Этот урок поможет вам преодолеть этот пробел.
необходимое условие
Я рекомендую вам следовать первой части серии, если вы еще этого не сделали. В первой части мы говорили об основах синтаксиса HOC и обо всем, что вам нужно, чтобы начать работу с компонентами высшего порядка.
В этом уроке мы будем опираться на концепции, которые мы уже рассмотрели в первой части. Я создал несколько образцов HOC, которые практически полезны, и вы можете включить эти идеи в свой проект. Фрагменты кода представлены в каждом разделе, а рабочая демонстрация всех практических HOC, обсуждаемых в этом учебном пособии, представлена в конце учебного пособия.
Вы также можете раскошелиться на мой репозиторий GitHub .
Практические компоненты высшего порядка
Поскольку HOC создают новый абстрактный контейнерный компонент, вот список вещей, которые вы обычно можете делать с ними:
- Оберните элемент или компонент вокруг компонента.
- Государственная абстракция.
- Управлять реквизитом, например, добавлять новые реквизиты и модифицировать или удалять существующие реквизиты.
- Реквизит проверки для создания.
- Используйте ссылки для доступа к методам экземпляра.
Давайте поговорим об этом один за другим.
HOC как компонент оболочки
Если вы помните, последний пример в моем предыдущем уроке продемонстрировал, как HOC объединяет InputComponent с другими компонентами и элементами. Это полезно для стилизации и повторного использования логики, где это возможно. Например, вы можете использовать эту технику для создания многоразового индикатора загрузчика или анимированного эффекта перехода, который должен вызываться определенными событиями.
Индикатор загрузки HOC
Первый пример — индикатор загрузки, построенный с использованием HOC. Он проверяет, является ли конкретный реквизит пустым, и индикатор загрузки отображается до тех пор, пока данные не будут получены и возвращены.
LoadIndicator / LoadIndicatorHOC.jsx
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
/* Method that checks whether a props is empty
prop can be an object, string or an array */
const isEmpty = (prop) => (
prop === null ||
prop === undefined ||
(prop.hasOwnProperty(‘length’) && prop.length === 0) ||
(prop.constructor === Object && Object.keys(prop).length === 0)
);
const withLoader = (loadingProp) => (WrappedComponent) => {
return class LoadIndicator extends Component {
render() {
return isEmpty(this.props[loadingProp]) ?
}
}
}
export default withLoader;
|
LoadIndicator / LoadIndicatorDemo.jsx
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
|
import React, { Component } from ‘react’;
import withLoader from ‘./LoaderHOC.jsx’;
class LoaderDemo extends Component {
constructor(props) {
super(props);
this.state = {
contactList: []
}
}
componentWillMount() {
let init = {
method: ‘GET’,
headers: new Headers(),
mode: ‘cors’,
cache: ‘default’
};
fetch
(‘https://demo1443058.mockable.io/users/’, init)
.then( (response) => (response.json()))
.then(
(data) => this.setState(
prevState => ({
contactList: […data.contacts]
})
)
)
}
render() {
return(
<div className=»contactApp»>
<ContactListWithLoadIndicator contacts = {this.state.contactList} />
</div>
)
}
}
const ContactList = ({contacts}) => {
return(
<ul>
{/* Code omitted for brevity */}
</ul>
)
}
/* Static props can be passed down as function arguments */
const ContactListWithLoadIndicator = withLoader(‘contacts’)(ContactList);
export default LoaderDemo;
|
Это также первый случай, когда мы использовали второй параметр в качестве входных данных для HOC. Второй параметр, который я назвал «loadingProp», используется здесь, чтобы сообщить HOC, что он должен проверить, выбран ли этот конкретный реквизит и доступен ли он. В этом примере функция isEmpty
проверяет, является ли loadingProp
пустым, и индикатор отображается до обновления реквизита.
У вас есть два варианта для передачи данных в HOC, либо в виде реквизита (что является обычным способом), либо в качестве параметра для HOC.
1
2
3
4
5
6
7
|
/* Two ways of passing down props */
<ContactListWithLoadIndicator contacts = {this.state.contactList} loadingProp= «contacts» />
//vs
const ContactListWithLoadIndicator = withLoader(‘contacts’)(ContactList);
|
Вот как я выбираю между двумя. Если у данных нет какой-либо области действия вне области HOC, и если данные являются статическими, то передайте их как параметры. Если реквизиты относятся к HOC, а также к обернутому компоненту, передайте их как обычные реквизиты. Я рассказал об этом подробнее в третьем уроке.
Государственная абстракция и опора Манипуляции
Абстракция состояния означает обобщение состояния на компонент более высокого порядка. Все управление состоянием WrappedComponent
будет обрабатываться компонентом высшего порядка. HOC добавляет новое состояние, и затем состояние передается в качестве реквизита для WrappedComponent
.
Универсальный контейнер высшего порядка
Если вы заметили, в приведенном выше примере загрузчика был компонент, который сделал запрос GET с использованием API выборки. После получения данных они были сохранены в состоянии. Выполнение запроса API при монтировании компонента является распространенным сценарием, и мы могли бы создать HOC, который идеально подходит для этой роли.
GenericContainer / GenericContainerHOC.jsx
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
import React, { Component } from ‘react’;
const withGenericContainer = ({reqUrl, reqMethod, resName}) => WrappedComponent => {
return class GenericContainer extends Component {
constructor(props) {
super(props);
this.state = {
[resName]: [],
}
}
componentWillMount() {
let init = {
method: reqMethod,
headers: new Headers(),
mode: ‘cors’,
cache: ‘default’
};
fetch(reqUrl, init)
.then( (response) => (response.json()))
.then(
(data) => {this.setState(
prevState => ({
[resName]: […data.contacts]
})
)}
)
}
render() {
return(
<WrappedComponent {…this.props} {…this.state} />)
}
}
}
export default withGenericContainer;
|
GenericContainer / GenericContainerDemo.jsx
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
/* A presentational component */
const GenericContainerDemo = () => {
return (
<div className=»contactApp»>
<ContactListWithGenericContainer />
</div>
)
}
const ContactList = ({contacts}) => {
return(
<ul>
{/* Code omitted for brevity */}
</ul>
)
}
/* withGenericContainer HOC that accepts a static configuration object.
The resName corresponds to the name of the state where the fetched data will be stored*/
const ContactListWithGenericContainer = withGenericContainer(
{ reqUrl: ‘https://demo1443058.mockable.io/users/’, reqMethod: ‘GET’, resName: ‘contacts’ })(ContactList);
|
Состояние было обобщено, и значение состояния передается как реквизит. Мы также сделали компонент настраиваемым.
1
2
3
|
const withGenericContainer = ({reqUrl, reqMethod, resName}) => WrappedComponent => {
}
|
Он принимает объект конфигурации в качестве входных данных, который предоставляет дополнительную информацию об URL-адресе API, методе и имени ключа состояния, в котором хранится результат. Логика, используемая в componentWillMount()
демонстрирует использование имени динамического ключа с this.setState
.
Форма высшего порядка
Вот еще один пример, который использует абстракцию состояния для создания полезного компонента формы высшего порядка.
CustomForm / CustomFormDemo.jsx
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
const Form = (props) => {
const handleSubmit = (e) => {
e.preventDefault();
props.onSubmit();
}
const handleChange = (e) => {
const inputName = e.target.name;
const inputValue = e.target.value;
props.onChange(inputName,inputValue);
}
return(
<div>
{/* onSubmit and onChange events are triggered by the form */ }
<form onSubmit = {handleSubmit} onChange={handleChange}>
<input name = «name» type= «text» />
<input name =»email» type=»text» />
<button type=»submit»> Submit </button>
</form>
</div>
)
}
const CustomFormDemo = (props) => {
return(
<div>
<SignupWithCustomForm {…props} />
</div>
);
}
const SignupWithCustomForm = withCustomForm({ contact: {name: », email: »}})({propName:’contact’, propListName: ‘contactList’})(Form);
|
CustomForm / CustomFormHOC.jsx
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
const CustomForm = (propState) => ({propName, propListName}) => WrappedComponent => {
return class withCustomForm extends Component {
constructor(props) {
super(props);
propState[propListName] = [];
this.state = propState;
this.handleSubmit = this.handleSubmit.bind(this);
this.handleChange = this.handleChange.bind(this);
}
/* prevState holds the old state.
handleSubmit() {
this.setState( prevState => {
return ({
[propListName]: […prevState[propListName], this.state[propName] ]
})}, () => console.log(this.state[propListName]) )}
/* When the input field value is changed, the [propName] is updated */
handleChange(name, value) {
this.setState( prevState => (
{[propName]: {…prevState[propName], [name]:value} }) )
}
render() {
return(
<WrappedComponent {…this.props} {…this.state} onChange = {this.handleChange} onSubmit = {this.handleSubmit} />
)
}
}
}
export default withCustomForm;
|
Пример демонстрирует, как абстракция состояния может использоваться вместе с презентационным компонентом, чтобы упростить создание формы. Здесь форма представляет собой презентационный компонент и является входной информацией для HOC. Начальное состояние формы и имя элементов состояния также передаются в качестве параметров.
1
2
3
4
|
const SignupWithCustomForm = withCustomForm
({ contact: {name: », email: »}}) //Initial state
({propName:’contact’, propListName: ‘contactList’}) //The name of state object and the array
(Form);
|
Тем не менее, обратите внимание, что если существует несколько реквизитов с одинаковыми именами, порядок важен, и последнее объявление реквизита всегда выигрывает. В этом случае, если другой компонент contactList
реквизит с именем contact
или contactList
, это приведет к конфликту имен. Таким образом, вы должны либо разместить пространство имен ваших реквизитов HOC, чтобы они не конфликтовали с существующими реквизитами, либо упорядочить их таким образом, чтобы реквизиты, которые должны иметь самый высокий приоритет, были объявлены первыми. Это будет подробно рассмотрено в третьем уроке.
Манипуляция опорой с использованием HOC
Манипулирование реквизитом включает добавление новых реквизитов, изменение существующих реквизитов или их полное игнорирование. В приведенном выше примере с CustomForm HOC передал несколько новых реквизитов.
1
|
<WrappedComponent {…this.props} {…this.state} onChange = {this.handleChange} onSubmit = {this.handleSubmit} />
|
Точно так же вы можете решить полностью игнорировать реквизит. Пример ниже демонстрирует этот сценарий.
1
2
3
4
|
// Technically an HOC
const ignoreHOC = (anything) => (props) => <h1> The props are ignored</h1>
const IgnoreList = ignoreHOC(List)()
<IgnoreList />
|
Вы также можете сделать некоторые проверки / фильтрации реквизита, используя эту технику. Компонент высшего порядка решает, должен ли дочерний компонент получать определенные реквизиты, или направлять пользователя к другому компоненту, если определенные условия не выполняются.
Компонент высшего порядка для защиты маршрутов
Вот пример защиты маршрутов путем обертывания соответствующего компонента компонентом высшего порядка withAuth
.
ProtectedRoutes / ProtectedRoutesHOC.jsx
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
const withAuth = WrappedComponent => {
return class ProtectedRoutes extends Component {
/* Checks whether the used is authenticated on Mount*/
componentWillMount() {
if (!this.props.authenticated) {
this.props.history.push(‘/login’);
}
}
render() {
return (
<div>
<WrappedComponent {…this.props} />
</div>
)
}
}
}
export default withAuth;
|
ProtectedRoutes / ProtectedRoutesDemo.jsx
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
import {withRouter} from «react-router-dom»;
class ProtectedRoutesDemo extends Component {
constructor(props) {
super(props);
/* Initialize state to false */
this.state = {
authenticated: false,
}
}
render() {
const { match } = this.props;
console.log(match);
return (
<div>
<ul className=»nav navbar-nav»>
<li><Link to={`${match.url}/home/`}>Home</Link></li>
<li><Link to={`${match.url}/contacts`}>Contacts(Protected Route)</Link></li>
</ul>
<Switch>
<Route exact path={`${match.path}/home/`} component={Home} />
<Route path={`${match.path}/contacts`} render={() => <ContactsWithAuth authenticated={this.state.authenticated} {…this.props} />} />
</Switch>
</div>
);
}
}
const Home = () => {
return (<div> Navigating to the protected route gets redirected to /login </div>);
}
const Contacts = () => {
return (<div> Contacts </div>);
}
const ContactsWithAuth = withRouter(withAuth(Contacts));
export default ProtectedRoutesDemo;
|
withAuth
проверяет, withAuth
ли пользователь, и, если нет, перенаправляет пользователя на /login.
Мы использовали withRouter
, который является сущностью реагирующего маршрутизатора. Интересно, что withRouter
также является компонентом высшего порядка, который используется для передачи обновленного соответствия, местоположения и истории реквизитам обернутого компонента каждый раз, когда он рендерится.
Например, он выдвигает объект истории как подпорки, чтобы мы могли получить доступ к этому экземпляру объекта следующим образом:
1
|
this.props.history.push(‘/login’);
|
Вы можете прочитать больше о withRouter
в официальной документации по реагирующему маршрутизатору .
Доступ к экземпляру через ссылки
У React есть специальный атрибут, который вы можете прикрепить к компоненту или элементу. Атрибут ref (ref обозначает ссылку) может быть функцией обратного вызова, прикрепленной к объявлению компонента.
Обратный вызов вызывается после монтирования компонента, и вы получаете экземпляр ссылочного компонента в качестве параметра обратного вызова. Если вы не уверены в том, как работают ссылки, в официальной документации по ссылкам и DOM об этом подробно говорится.
В нашем HOC преимущество ref заключается в том, что вы можете получить экземпляр WrappedComponent
и вызвать его методы из компонента высшего порядка. Это не является частью типичного потока данных React, потому что React предпочитает связь через реквизит. Однако есть много мест, где вы можете найти этот подход полезным.
RefsDemo / RefsHOC.jsx
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
const withRefs = WrappedComponent => {
return class Refs extends Component {
constructor(props) {
super(props);
this.state = {
value: »
}
this.setStateFromInstance = this.setStateFromInstance.bind(this);
}
/* This method calls the Wrapped component instance method getCurrentState */
setStateFromInstance() {
this.setState({
value: this.instance.getCurrentState()
})
}
render() {
return(
<div>
{ /* The ref callback attribute is used to save a reference to the Wrapped component instance */ }
<WrappedComponent {…this.props} ref= { (instance) => this.instance = instance } />
<button onClick = {this.
<h3> The value is {this.state.value} </h3>
</div>
);
}
}
}
|
RefsDemo / RefsDemo.jsx
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
const RefsDemo = () => {
return (<div className=»contactApp»>
<RefsComponent />
</div>
)
}
/* A typical form component */
class SampleFormComponent extends Component {
constructor(props) {
super(props);
this.state = {
value: »
}
this.handleChange = this.handleChange.bind(this);
}
getCurrentState() {
console.log(this.state.value)
return this.state.value;
}
handleChange(e) {
this.setState({
value: e.target.value
})
}
render() {
return (
<input type=»text» onChange={this.handleChange} />
)
}
}
const RefsComponent = withRefs(SampleFormComponent);
|
Атрибут ref
callback сохраняет ссылку на WrappedComponent
.
1
|
<WrappedComponent {…this.props} ref= { (instance) => this.instance = instance } />
|
this.instance
имеет ссылку на WrappedComponent
. Теперь вы можете вызвать метод экземпляра для обмена данными между компонентами. Однако используйте это экономно и только при необходимости.
Финальная демонстрация
Я включил все примеры в этом уроке в одну демонстрацию. Просто клонируйте или загрузите исходный код с GitHub, и вы можете попробовать его сами.
Чтобы установить зависимости и запустить проект, просто выполните следующие команды из папки проекта.
1
2
|
npm install
npm start
|
Резюме
Это конец второго урока по компонентам высшего порядка. Сегодня мы многое узнали о различных шаблонах и методах HOC и рассмотрели практические примеры, демонстрирующие, как мы можем использовать их в наших проектах.
В третьей части руководства вы можете ознакомиться с некоторыми передовыми методами и альтернативами HOC, о которых вам следует знать. Оставайтесь с нами до тех пор. Поделитесь своими мыслями в поле для комментариев.