Использование шаблона render props вне рендера

в 11:28, , рубрики: javascript, React, react 16+, ReactJS

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

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

Автор: maxfarseer

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js