Статьи

Нежное Введение в HOC в Реакте: Учитесь на Примере

Это вторая часть серии о компонентах высшего порядка (HOCs). Сегодня я расскажу о различных шаблонах компонентов высшего порядка, которые полезны и реализуемы. С помощью HOC вы можете абстрагировать избыточный код в слой более высокого порядка. Однако, как и любые другие шаблоны, привыкание к HOC займет некоторое время. Этот урок поможет вам преодолеть этот пробел.

Я рекомендую вам следовать первой части серии, если вы еще этого не сделали. В первой части мы говорили об основах синтаксиса HOC и обо всем, что вам нужно, чтобы начать работу с компонентами высшего порядка.

В этом уроке мы будем опираться на концепции, которые мы уже рассмотрели в первой части. Я создал несколько образцов HOC, которые практически полезны, и вы можете включить эти идеи в свой проект. Фрагменты кода представлены в каждом разделе, а рабочая демонстрация всех практических HOC, обсуждаемых в этом учебном пособии, представлена ​​в конце учебного пособия.

Вы также можете раскошелиться на мой репозиторий GitHub .

Поскольку HOC создают новый абстрактный контейнерный компонент, вот список вещей, которые вы обычно можете делать с ними:

  • Оберните элемент или компонент вокруг компонента.
  • Государственная абстракция.
  • Управлять реквизитом, например, добавлять новые реквизиты и модифицировать или удалять существующие реквизиты.
  • Реквизит проверки для создания.
  • Используйте ссылки для доступа к методам экземпляра.

Давайте поговорим об этом один за другим.

Если вы помните, последний пример в моем предыдущем уроке продемонстрировал, как HOC объединяет InputComponent с другими компонентами и элементами. Это полезно для стилизации и повторного использования логики, где это возможно. Например, вы можете использовать эту технику для создания многоразового индикатора загрузчика или анимированного эффекта перехода, который должен вызываться определенными событиями.

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

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;
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, который идеально подходит для этой роли.

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;
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 .

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

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);
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, чтобы они не конфликтовали с существующими реквизитами, либо упорядочить их таким образом, чтобы реквизиты, которые должны иметь самый высокий приоритет, были объявлены первыми. Это будет подробно рассмотрено в третьем уроке.

Манипулирование реквизитом включает добавление новых реквизитов, изменение существующих реквизитов или их полное игнорирование. В приведенном выше примере с 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 .

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;
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 предпочитает связь через реквизит. Однако есть много мест, где вы можете найти этот подход полезным.

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>
        );
      }
    }
}
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, о которых вам следует знать. Оставайтесь с нами до тех пор. Поделитесь своими мыслями в поле для комментариев.