React представляет новое API (context API), которое использует "паттерн" (шаблон) render props
(подробнее). На семинарах, встречах и в твиттере я вижу, что возникает много вопросов об использовании render props
вне рендера, например, в обработчиках событий или "хуках" жизненного цикла(`lifecycle hooks').
Полтора года назад, когда я работал над 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/>
.
Итак, теперь у вас есть все необходимое. Все очевидно, стоит лишь раз увидеть, а ведь до этого все казалось сложным.
rzcoder
Был хороший context api который прекрасно решал свои задачи. Абрамов решил «улучшить» частный случай использования, сломав все остальные сценарии: использование вне рендера, простое использование нескольких контекстов одновременно. Теперь, чтобы всё «починить» нам предлагают использовать bind в рендере (что тысячу раз уже заклеймили антипаттерном) и оборачивать компоненты в компоненты, только ради того чтобы решить задачу которая раньше не требовала решения вообще.
faiwer
Ну и помимо прочего даже "нативное" применение consumer-а теперь сопровождается callback-ом. Элегантным такое решение назвать никак не получается. Кто-то очень любит матрёшки. Особенно они изящно выглядят, когда нужно несколько полей из context-а. Интересно, как скоро мы придём к
iojsфорку React-а? :)Особенно меня впечатляют вот такие вот публикации. Такие восторги, охи и ахи. "I’m excited about what this new API has to offer".
rzcoder
Думаю форк случится на моменте выпуска react 17, где они уберут componentWillMount и сделают shouldComponentUpdate статичным методом. Фактически разделят экосистему реакта на до и после 17 версии.