3.12 Паттерн: свойство render
Термин «свойство render» (в оригинале «render prop») относится к простой методике совместного использования кода
между компонентами React, принимающими свойство prop
, значение которого является функцией.
Компонент со свойством render
принимает функцию, которая возвращает элемент React,
и вызывает её вместо реализации своей собственной логики отрисовки.
<DataProvider render={data => (
<h1>Hello {data.target}</h1>
)}/>
Библиотеками, использующими такой подход, являются React Router и .
В этом разделе мы обсудим, почему полезно использовать свойство для отрисовки.
Компоненты являются основным элементом повторного использования кода в 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: '100%' }} onMouseMove={this.handleMouseMove}>
<h1>Move the mouse around!</h1>
<p>The current mouse position is ({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: '100%' }} onMouseMove={this.handleMouseMove}>
{/* ...но как мы отрисуем что-то отличное от <p>? */}
<p>Текущая позиция мыши: ({this.state.x}, {this.state.y})</p>
</div>
);
}
}
class MouseTracker extends React.Component {
render() {
return (
<div>
<h1>Переместите мышь!</h1>
<Mouse />
</div>
);
}
}
Теперь компонент <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: '100%' }} onMouseMove={this.handleMouseMove}>
{/*
Здесь мы без труда можем поменять тег <p> на <Cat>... но тогда
нам придется создавать отдельный компонент <MouseСЧемТоЕще>
каждый раз, когда он нам будет необходим, поэтому <MouseWithCat>
в действительности не является повторно используемым.
*/}
<Cat mouse={this.state} />
</div>
);
}
}
class MouseTracker extends React.Component {
render() {
return (
<div>
<h1>Переместите мышь!</h1>
<MouseWithCat />
</div>
);
}
}
Такой подход будет работать для нашего конкретного случая, но мы не достигли
цели по-настоящему инкапсулировать поведение для повторного использования. Теперь, каждый раз,
когда нам нужна позиция мыши для какого-нибудь другого случая, мы должны создать новый компонент (т. е. по
существу другой <MouseWithCat>
), который отрисовывает что-то конкретное для данного случая.
Вот где в силу вступает паттерн «свойство render»: вместо жесткого кодирования <Cat>
внутри компонента <Mouse>
и изменения результата его отрисовки, мы можем предоставить
компоненту <Mouse>
особое свойство-функцию, которое он использует для динамического определения того,
что необходимо отрисовать. Это особое свойство и есть «свойство render».
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: '100%' }} onMouseMove={this.handleMouseMove}>
{/*
Вместо задания статического представления того, что должен отрисовывать <Mouse>,
используйте свойство 'render' чтобы определять это представление динамически.
*/}
{this.props.render(this.state)}
</div>
);
}
}
class MouseTracker extends React.Component {
render() {
return (
<div>
<h1>Move the mouse around!</h1>
<Mouse render={mouse => (
<Cat mouse={mouse} />
)}/>
</div>
);
}
}
Теперь, вместо клонирования компонента <Mouse>
и жесткого кодирования чего-то еще в его render
методе для конкретного варианта использования, мы предоставляем свойство render
, которое <Mouse>
может использовать для динамического определения того, что он отрисовывает.
Более конкретно: свойство render
является свойством-функцией, которую компонент использует,
для определения того, что ему необходимо отрисовывать.
Данный подход делает поведение, которое нам необходимо для совместного использования, чрезвычайно
портативным. Чтобы получить это поведение, отрисуйте компонент <Mouse>
с помощью свойства render
,
которое сообщает ему, что необходимо отрисовать, используя текущее положение (x, y)
курсора.
Необходимо отметить одну важную деталь о свойстве render
: вы можете реализовать большинство
компонентов более высокого порядка (HOC), используя обычный компонент со свойством render
. Например,
если вы предпочли бы иметь такой HOC как withMouse вместо <Mouse>
, вы могли бы легко создать
его с помощью обычного компонента <Mouse>
со свойством render
:
// Если вам действительно по какой-то причине необходим HOC, вы можете без труда
// создать его, используя обычный компонент со свойством render
function withMouse(Component) {
return class extends React.Component {
render() {
return (
<Mouse render={mouse => (
<Component {...this.props} mouse={mouse} />
)}/>
);
}
}
}
Таким образом, использование свойства render
дает возможность применить любой паттерн.
Важно помнить, что только потому, что паттерн называется «свойство render», вам не
обязательно использовать свойство с именем render
для реализации этого паттерна. Фактически,
любое свойство, которое является функцией и которое компонент использует, чтобы определить, что
ему необходимо отрисовать
, технически является паттерном «свойство render».
Хотя приведенные выше примеры используют свойство render
, мы могли бы
так же легко использовать свойство children
!
<Mouse children={mouse => (
<p>The mouse position is {mouse.x}, {mouse.y}</p>
)}/>
И помните, что на самом деле нет необходимости указывать свойство children
в
списке «атрибутов» вашего JSX-элемента. Вместо этого вы можете поместить его прямо внутри этого элемента!
<Mouse>
{mouse => (
<p>Позиция мыши: {mouse.x}, {mouse.y}</p>
)}
</Mouse>
Вы можете увидеть этот подход в API.
Поскольку этот метод немного необычен, вы при разработке API, вероятно, захотите явно указать, что свойство
children
должно быть функцией в ваших propTypes
, как здесь:
Mouse.propTypes = {
children: PropTypes.func.isRequired
};
Использование свойства render
может свести на нет все преимущество, которое дает
использование React.PureComponent
, если вы создаете функцию внутри метода render
.
Это происходит из-за того, что неглубокое сравнение свойств props
всегда возвращает false
для новых свойств props
, и каждая отрисовка в этом случае генерирует новое значение для свойства 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>
, тем самым сводя на нет эффект
наследования <Mouse>
от React.PureComponent
!
Чтобы обойти эту проблему, иногда вы можете определить свойство как метод экземпляра, например:
class MouseTracker extends React.Component {
renderTheCat(mouse) {
// Определённый как член класса, `this.renderTheCat` всегда ссылается на
// ту же самую функцию, когда мы используем ее в render
return <Cat mouse={mouse} />;
}
render() {
return (
<div>
<h1>Переместите мышь!</h1>
<Mouse render={this.renderTheCat} />
</div>
);
}
}
В случаях, когда вы не можете определить свойство статически,
компонент <Mouse>
должен наследоваться от React.Component
.