React представляет новое API (context API), которое использует "паттерн" (шаблон) render props (подробнее). На семинарах, встречах и в твиттере я вижу, что возникает много вопросов об использовании render props вне рендера, например, в обработчиках событий или "хуках" жизненного цикла(`lifecycle hooks').


image


Полтора года назад, когда я работал над React Router v4, меня особенно заинтересовало, как раз и навсегда решить проблему "глубоких обновлений". Я создал библиотеку с названием react-context-emission (позднее — react-broadcast) с API концептуально идентичным тому, что представил React в своем новом context API.


// React context emission API
const { LocationEmitter, LocationSubscriber } = createContextEmission('location')
<LocationEmitter location={value}/>
<LocationSubscriber>{({ location }) => (...)}</LocationSubscriber>

// Новое React Context API
const { Provider, Consumer } = React.createContext()
<Provider value={location}/>
<Consumer>{value => (...)}</Consumer>

После использования этого шаблона мне действительно понравилось связывать значения переменных с компонентами через контекст и отрисовывать свойства (рендерить props), однако я изо всех сил старался получить доступ к контекстным значениям за пределами рендеринга. В моей реализации я привык получать их из this.context повсеместно. Это было одной из причин, по которой мы вернулись к использованию текущего (устаревшего?) contextTypes API в React Router.


Решить эту проблему не так сложно. Чтобы понять это, мне потребовалось время. Однако, как только вы увидите решение, оно покажется вам очевидным. Убедитесь сами!


Доступ к значениям в обработчиках событий


Нужно лишь… передать значение в обработчик:


class Something extends React.Component {
  handleClick = (event, stuff) => {
    console.log(stuff);
  };

  render() {
    return (
      <SomeContext.Consumer>
        {stuff => (
          <div>
            <h1>Cool! {stuff}</h1>
            <button onClick={event => this.handleClick(event, stuff)}>
              Click me
            </button>
          </div>
        )}
      </SomeContext.Consumer>
    );
  }
}

Доступ к значениям в lifecycle hooks


В случае с lifecycle hooks предыдущий шаблон не работает, т.к. не мы вызываем хуки, а React. Предлагаю три шаблона, которые я использовал, выбирайте, который больше понравится (третий — мой любимый!).


Оборачиваем


Вы можете получить доступ к данным путем создания двух компонентов: контейнера, который использует контекст, и контейнера, который принимает контекст как свойство.


// Поглотите его (имеется, в виду компонент)
const SomethingContainer = () => (
  <SomeContext.Consumer>
    {stuff => <Something stuff={stuff} />}
  </SomeContext.Consumer>
);

// Вуаля, получите stuff в prop! Context в ваших методах жизненного цикла
class Something extends React.Component {
  componentDidMount() {
    console.log(this.props.stuff);
  }

  render() {
    return (
      <div>
        <h1>Cool! {this.props.stuff}</h1>
      </div>
    );
  }
}

Создаем компоненты высшего порядка (HOC-компоненты)


Возможно, вы уже привыкли декорировать одни компоненты другими (например, с помощью HOC — High Order Component). Можно довольно быстро превратить render prop компонент в компонент высшего порядка. Я не особо люблю такой способ, поскольку для реализации он требует значительные перетасовки в коде и множество концепций.


// HOC
const withStuff = Comp => props => (
  <SomeContext.Consumer>
    {stuff => <Comp stuff={stuff} />}
  </SomeContext.Consumer>
);

// Декорированный класс
class SomethingImpl extends React.Component {
  componentDidMount() {
    console.log(this.props.stuff);
  }

  render() {
    return (
      <div>
        <h1>Cool! {this.props.stuff}</h1>
      </div>
    );
  }
}

// the actual decoration
const Something = withStuff(SomethingImpl)

Компонентный компонент: моя новая любовь.


Однажды я создал компонент, который просто брал функцию как свойство и вызывал функцию в componentDidUpdate. Я уже делал свойство с именем render и теперь у меня появилось еще одно под названием didUpdate. Я понял, что можно преобразовать каждый метод класса компонента в свойство компонента, и вот так появился @reactions/component!


Очень удобно компоновать render props в хуках жизненного цикла без всяких перетасовок в коде:


import Component from '@reactions/component';

const Something = () => (
  <SomeContext.Consumer>
    {stuff => (
      <Component didMount={() => console.log(stuff)}>
        <h1>Cool! {stuff}</h1>
      </Component>
    )}
  </SomeContext.Consumer>
);

Обычно свойства можно сравнить в componentDidUpdate, это тоже неплохо работает:


const Something = () => (
  <SomeContext.Consumer>
    {stuff => (
      <Component
        stuff={stuff}
        didUpdate={({prevProps, props}) => {
          console.log(prevProps.stuff === props.stuff);
        }}
      >
        <h1>Cool! {stuff}</h1>
      </Component>
    )}
  </SomeContext.Consumer>
);

Это мой любимый метод. Код может изменяться и перемещаться без каких-либо несоответствий, поскольку он не несет новых концепций, вся структура организуется компонентами. Если вы больше не нуждаетесь в методах жизненных циклов, вам не надо распутывать абстракцию, просто удалите <Component/>.


Итак, теперь у вас есть все необходимое. Все очевидно, стоит лишь раз увидеть, а ведь до этого все казалось сложным.

Комментарии (4)


  1. rzcoder
    15.03.2018 10:17
    +1

    Был хороший context api который прекрасно решал свои задачи. Абрамов решил «улучшить» частный случай использования, сломав все остальные сценарии: использование вне рендера, простое использование нескольких контекстов одновременно. Теперь, чтобы всё «починить» нам предлагают использовать bind в рендере (что тысячу раз уже заклеймили антипаттерном) и оборачивать компоненты в компоненты, только ради того чтобы решить задачу которая раньше не требовала решения вообще.


    1. faiwer
      15.03.2018 10:36

      Ну и помимо прочего даже "нативное" применение consumer-а теперь сопровождается callback-ом. Элегантным такое решение назвать никак не получается. Кто-то очень любит матрёшки. Особенно они изящно выглядят, когда нужно несколько полей из context-а. Интересно, как скоро мы придём к iojsфорку React-а? :)


      Особенно меня впечатляют вот такие вот публикации. Такие восторги, охи и ахи. "I’m excited about what this new API has to offer".


      1. rzcoder
        15.03.2018 13:49

        Думаю форк случится на моменте выпуска react 17, где они уберут componentWillMount и сделают shouldComponentUpdate статичным методом. Фактически разделят экосистему реакта на до и после 17 версии.


  1. K3rLa3da
    16.03.2018 09:45

    Там-то ещё и willReceiveProps уберут