Рендер-пропсы
Термин «рендер-проп» относится к возможности компонентов React разделять код между собой с помощью пропа, значение которого является функцией.
Компонент с рендер-пропом берёт функцию, которая возвращает React-элемент, и вызывает её вместо реализации собственного рендера.
<DataProvider render={data => (
<h1>Привет, {data.target}</h1>
)}/>
Такой подход, в частности, применяется в библиотеках React Router, Downshift и Formik.
В этой статье мы покажем, чем полезны и как писать рендер-пропсы.
Использование рендер-пропа для сквозных задач
Компоненты — это основа повторного использования кода в 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>,
используем рендер-проп для динамического определения, что надо отрендерить.
*/}
{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
, для решения специфичного случая, мы предоставляем рендер-проп компоненту <Mouse>
, который может динамически определить что рендерить.
Иными словами, рендер-проп — функция, которая сообщает компоненту что необходимо рендерить.
Эта техника позволяет сделать легко портируемым поведение, которое мы хотим повторно использовать. Для этого следует отрендерить компонент <Mouse>
с помощью рендер-пропа, который сообщит, где отрендерить курсор с текущим положением (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.
Поскольку этот метод не совсем обычен, вы, вероятно, захотите явно указать, что children
должен быть функцией в вашем propTypes
при разработке такого API.
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>
{/*
Это плохо! Значение рендер-пропа будет
разным при каждом рендере.
*/}
<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
.