Рендер-пропси
Термін “рендер-проп” відноситься до техніки, в якій React-компоненти розділяють між собою один код (функцію) передаючи її через проп.
Компонент з рендер-пропом приймає функцію, яка повертає React-елемент, і викликає її замість реалізації власної рендер-логіки.
<DataProvider render={data => (
<h1>Привіт {data.target}</h1>
)}/>
Такі бібліотеки, як React Router, Downshift та Formik використовують рендер-пропси.
На цій сторінці ми розглянемо чим рендер-пропси корисні та як їх писати.
Використання рендер-пропсів для наскрізних завдань (Cross-Cutting Concerns)
Компоненти — основа повторного використання коду в React. Але не завжди буває очевидно, як інкапсульовані в одному компоненті стан чи поведінку розділити з іншими компонентами, що їх потребують.
Наприклад, наступний компонент відслідковує позицію курсора миші у веб-додатку:
class MouseTracker extends React.Component {
constructor(props) {
super(props);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.state = { x: 0, y: 0 };
}
handleMouseMove(event) {
this.setState({
x: event.clientX,
y: event.clientY
});
}
render() {
return (
<div style={{ height: '100vh' }} onMouseMove={this.handleMouseMove}>
<h1>Переміщуйте курсор миші!</h1>
<p>Поточна позиція курсора миші: ({this.state.x}, {this.state.y})</p>
</div>
);
}
}
По мірі переміщення курсора, компонент виводить його координати (x, y) всередині блоку <p>
.
Виникає питання: як ми можемо повторно використати цю ж поведінку в іншому компоненті? Іншими словами, якщо інший компонент потребує знати позицію курсора, чи можемо ми якимось чином інкапсулювати цю поведінку, щоб потім легко використати її в цьому компоненті?
Оскільки компоненти являються базовою одиницею повторного використання коду в React, давайте зробимо невеликий рефакторинг. Виділимо компонент <Mouse>
, який інкапсулюватиме поведінку, яку б ми хотіли повторно використовувати в нашому коді.
// Компонент <Mouse> інкапсулює потрібну нам поведінку...
class Mouse extends React.Component {
constructor(props) {
super(props);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.state = { x: 0, y: 0 };
}
handleMouseMove(event) {
this.setState({
x: event.clientX,
y: event.clientY
});
}
render() {
return (
<div style={{ height: '100vh' }} onMouseMove={this.handleMouseMove}>
{/* ...але як вивести щось, окрім тегу <p>? */}
<p>Поточна позиція курсора миші: ({this.state.x}, {this.state.y})</p>
</div>
);
}
}
class MouseTracker extends React.Component {
render() {
return (
<>
<h1>Переміщуйте курсор миші!</h1>
<Mouse />
</>
);
}
}
Тепер компонент <Mouse>
інкапсулює в собі всю поведінку, пов’язану з реагуванням на події mousemove
та зберіганням позиції (x, y) курсора, але він поки ще не достатньо гнучкий для повторного використання.
Наприклад, скажімо в нас є компонент <Cat>
, який рендерить зображення кота, що ганяється за мишкою по екрану. Ми можемо використати проп <Cat mouse={{ x, y }}>
для передачі компоненту координати миші, щоб він знав де розмістити зображення на екрані.
Спочатку ви можете спробувати рендерити <Cat>
всередині методу render
компонента <Mouse>
, наприклад так:
class Cat extends React.Component {
render() {
const mouse = this.props.mouse;
return (
<img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} />
);
}
}
class MouseWithCat extends React.Component {
constructor(props) {
super(props);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.state = { x: 0, y: 0 };
}
handleMouseMove(event) {
this.setState({
x: event.clientX,
y: event.clientY
});
}
render() {
return (
<div style={{ height: '100vh' }} onMouseMove={this.handleMouseMove}>
{/*
Ми могли б тут просто замінити тег <p> на компонент <Cat>... але тоді
нам потрібно було б створювати окремий компонент <MouseWithSomethingElse>
кожного разу, коли він нам потрібен, тому <MouseWithCat>
поки що не достатньо гнучкий для повторного використання.
*/}
<Cat mouse={this.state} />
</div>
);
}
}
class MouseTracker extends React.Component {
render() {
return (
<div>
<h1>Переміщуйте курсор миші!</h1>
<MouseWithCat />
</div>
);
}
}
Цей підхід працюватиме в нашому конкретному випадку, але ми й досі не досягли мети — інкапсуляції поведінки з можливістю повторного використання. Тепер кожен раз, коли нам потрібно отримати позицію курсора миші для різних варіантів, нам прийдеться створювати новий компонент (тобто по суті ще один <MouseWithCat>
), який рендерить щось спеціально для цього випадку.
Ось тут нам і знадобиться рендер-проп. Замість явного задавання <Cat>
всередині компонента <Mouse>
і зміни результату рендеру таким чином, ми можемо передавати компоненту <Mouse>
функцію через проп (рендер-проп), яку він використає для динамічного визначення того, що потрібно рендерити.
class Cat extends React.Component {
render() {
const mouse = this.props.mouse;
return (
<img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} />
);
}
}
class Mouse extends React.Component {
constructor(props) {
super(props);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.state = { x: 0, y: 0 };
}
handleMouseMove(event) {
this.setState({
x: event.clientX,
y: event.clientY
});
}
render() {
return (
<div style={{ height: '100vh' }} onMouseMove={this.handleMouseMove}>
{/*
Замість статичного декларування того, що рендерить <Mouse>, використовуємо
проп `render` для динамічного визначення того, що потрібно відрендерити.
*/}
{this.props.render(this.state)}
</div>
);
}
}
class MouseTracker extends React.Component {
render() {
return (
<div>
<h1>Переміщуйте курсор миші!</h1>
<Mouse render={mouse => (
<Cat mouse={mouse} />
)}/>
</div>
);
}
}
Тепер замість того, щоб фактично клонувати компонент <Mouse>
та жорстко задавати щось інше в його методі render
для рішення конкретного випадку, ми надаємо проп render
, який <Mouse>
може використати для динамічного визначення того, що він рендерить.
Більш конкретно, рендер-проп — це функція, передана як проп, яку компонент використовує, щоб визначити що рендерити.
Ця техніка робить надзвичайно портативною поведінку, яку ми хотіли б використовувати повторно. Для отримання потрібної поведінки ми тепер рендеримо компонент <Mouse>
з пропом render
, який вказує що відрендерити для поточних координат (x, y) курсора.
Одна цікава річ яку варто відзначити про рендер-пропси полягає в тому, що ви можете реалізувати більшість компонентів вищого порядку (HOC) з використанням звичайного компоненту з рендер-пропом. Наприклад, якщо ви надаєте перевагу HOC withMouse
замість компонента <Mouse>
, ви могли б легко його створити з використанням звичайного компоненту <Mouse>
та рендер-пропу:
// Якщо з певних причин вам дійсно потрібен HOC, ви можете легко
// його створити з використанням звичайного компонента і рендер-пропа!
function withMouse(Component) {
return class extends React.Component {
render() {
return (
<Mouse render={mouse => (
<Component {...this.props} mouse={mouse} />
)}/>
);
}
}
}
Таким чином, використання рендер-пропу дозволяє реалізувати будь-який з наведених вище патернів.
Використання пропсів, відмінних від render
Важливо пам’ятати, що із назви патерну “рендер-пропси” зовсім не слідує, що для його використання ви повинні використовувати проп з ім’ям render
. Насправді будь-який проп, який є функцією і використовується компонентом для визначення того, що рендерити, технічно є “рендер-пропом”.
Незважаючи на те, що наведені приклади використовують проп render
, ми могли б так само легко використати проп children
!
<Mouse children={mouse => (
<p>Поточна позиція курсора миші: {mouse.x}, {mouse.y}</p>
)}/>
Також запам’ятайте, що проп children
не обов’язково повинен бути зазначений у списку «атрибутів» у вашому JSX-елементі. Замість цього, ви можете помістити його прямо всередину елемента!
<Mouse>
{mouse => (
<p>Поточна позиція курсора миші: {mouse.x}, {mouse.y}</p>
)}
</Mouse>
Ви побачите, що ця техніка використовується в API бібліотеки react-motion.
Оскільки ця техніка дещо незвична, то при розробці такого API було б доречно явно вказати в propTypes
, що проп children
повинен бути функцією.
Mouse.propTypes = {
children: PropTypes.func.isRequired
};
Застереження
Будьте обережні при використанні рендер-пропсів разом з React.PureComponent
Використання рендер-проп може звести нанівець переваги, що надає React.PureComponent
, якщо ви створюєте функцію всередині методу render
. Це спричинене тим, що поверхове порівняння пропсів завжди повертатиме false
для нових пропсів, а в даному випадку кожен виклик render
генеруватиме нове значення для рендер-пропа.
Наприклад, продовжуючи з нашим вищезгаданим компонентом <Mouse>
, якби Mouse
наслідував React.PureComponent
замість React.Component
, наш приклад виглядав би наступним чином:
class Mouse extends React.PureComponent {
// Така ж сама реалізація, як і раніше...
}
class MouseTracker extends React.Component {
render() {
return (
<div>
<h1>Переміщуйте курсор миші!</h1>
{/*
Погано! Значення пропа `render` буде
різним при кожному рендері.
*/}
<Mouse render={mouse => (
<Cat mouse={mouse} />
)}/>
</div>
);
}
}
У цьому прикладі при кожному рендері <MouseTracker>
, генерується нова функція в якості значення пропу <Mouse render>
, таким чином зводячи нанівець ефект React.PureComponent
, який <Mouse>
наслідує!
Щоб вирішити цю проблему, ви можете визначити проп як метод екземпляру, наприклад так:
class MouseTracker extends React.Component {
// Визначаємо метод екземпляру, тепер `this.renderTheCat` завжди
// посилається на *ту саму* функцію, коли ми використовуємо його в рендері
renderTheCat(mouse) {
return <Cat mouse={mouse} />;
}
render() {
return (
<div>
<h1>Переміщуйте курсор миші!</h1>
<Mouse render={this.renderTheCat} />
</div>
);
}
}
В тих випадках, коли ви не можете статично задати проп (наприклад тому, що вам потрібно замкнути пропси та/або стан компоненту), <Mouse>
повинно наслідувати React.Component
.