Компоненты с передачей рендеринга
Термин “передача рендеринга” относится к технике совместного использования кода между компонентами React с помощью свойства, значением которого является функция.
Компонент с передачей рендеринга принимает функцию, возвращающую React-элемент, и вызывает её вместо реализации собственной логики рендеринга.
<DataProvider render={data => (
<h1>Hello {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>Move the mouse around!</h1>
<p>The current mouse position is ({this.state.x}, {this.state.y})</p>
</div>
);
}
} По мере перемещения курсора по экрану компонент отображает его координаты (x, y) в <p>.
Теперь вопрос в том, как мы можем повторно использовать это поведение в другом компоненте? Другими словами, если другому компоненту нужно знать положение курсора, можем ли мы инкапсулировать это поведение таким образом, чтобы легко поделиться им с этим компонентом?
Поскольку компоненты являются основным элементом повторного использования кода в React, давайте попробуем немного переработать код, чтобы использовать компонент <Mouse>, который инкапсулирует необходимое поведение, которое нужно повторно использовать в других местах.
// The <Mouse> component encapsulates the behavior we need...
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}>
{/* ...but how do we render something other than a <p>? */}
<p>The current mouse position is ({this.state.x}, {this.state.y})</p>
</div>
);
}
}
class MouseTracker extends React.Component {
render() {
return (
<>
<h1>Move the mouse around!</h1>
<Mouse />
</>
);
}
} Теперь компонент <Mouse> инкапсулирует всё поведение, связанное с прослушиванием mousemove событий и сохранением положения курсора (x, y), но он пока не является по-настоящему повторно используемым.
Например, предположим, что у нас есть компонент <Cat>, который отображает изображение кошки, преследующей мышь по экрану. Мы можем использовать свойство <Cat mouse={{ x, y }}> для передачи компоненту координат мыши, чтобы он знал, где расположить изображение на экране.
В качестве первого шага вы можете попробовать отобразить <Cat> внутри метода <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 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}>
{/*
We could just swap out the <p> for a <Cat> here ... but then
we would need to create a separate <MouseWithSomethingElse>
component every time we need to use it, so <MouseWithCat>
isn't really reusable yet.
*/}
<Cat mouse={this.state} />
</div>
);
}
}
class MouseTracker extends React.Component {
render() {
return (
<div>
<h1>Move the mouse around!</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}>
{/*
Instead of providing a static representation of what <Mouse> renders,
use the `render` prop to dynamically determine what to 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> может использовать для динамического определения отображаемого контента.
Более конкретно, передача рендеринга — это свойство-функция, которое компонент использует для определения того, что отображать.
Эта техника делает необходимое для совместного использования поведение чрезвычайно портативным. Для получения этого поведения отобразите компонент <Mouse> со свойством render, которое указывает, что отображать с текущими координатами (x, y) курсора.
Одна интересная особенность передачи рендеринга заключается в том, что большинство высокоуровневых компонентов (HOC) можно реализовать с помощью обычного компонента с передачей рендеринга. Например, если вы предпочитаете иметь HOC withMouse вместо компонента <Mouse>, вы можете легко создать его, используя обычный компонент <Mouse> с передачей рендеринга:
// If you really want a HOC for some reason, you can easily
// create one using a regular component with a render prop!
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>The mouse position is {mouse.x}, {mouse.y}</p>
)}/> И помните, свойство children фактически не нужно указывать в списке «атрибутов» в вашем элементе JSX. Вместо этого его можно поместить непосредственно внутри элемента!
<Mouse>
{mouse => (
<p>The mouse position is {mouse.x}, {mouse.y}</p>
)}
</Mouse> Вы увидите эту технику в API react-motion.
Поскольку эта техника немного необычна, при проектировании API таким образом вы, вероятно, захотите явно указать, что children должно быть функцией в вашем propTypes.
Mouse.propTypes = {
children: PropTypes.func.isRequired
}; Ограничения
Будьте внимательны при использовании передачи рендеринга с React.PureComponent
Использование передачи рендеринга может аннулировать преимущества использования React.PureComponent, если вы создаёте функцию внутри метода render. Это происходит потому, что сравнение свойств по поверхностным данным всегда вернёт значение false для новых свойств, и каждый render в этом случае сгенерирует новое значение для передачи рендеринга.
Например, продолжим наш компонент <Mouse> из примера выше. Если Mouse будет расширять React.PureComponent вместо React.Component, наш пример будет выглядеть так:
class Mouse extends React.PureComponent {
// Same implementation as above...
}
class MouseTracker extends React.Component {
render() {
return (
<div>
<h1>Move the mouse around!</h1>
{/*
This is bad! The value of the `render` prop will
be different on each render.
*/}
<Mouse render={mouse => (
<Cat mouse={mouse} />
)}/>
</div>
);
}
} В этом примере каждый раз, когда <MouseTracker> отображается, он генерирует новую функцию как значение свойства <Mouse render>, тем самым аннулируя эффект от расширения <Mouse> компонентом React.PureComponent в первую очередь!
Чтобы обойти эту проблему, вы иногда можете определить свойство как метод экземпляра, как показано ниже:
class MouseTracker extends React.Component {
// Defined as an instance method, `this.renderTheCat` always
// refers to *same* function when we use it in render
renderTheCat(mouse) {
return <Cat mouse={mouse} />;
}
render() {
return (
<div>
<h1>Move the mouse around!</h1>
<Mouse render={this.renderTheCat} />
</div>
);
}
} В случаях, когда вы не можете статически определить свойство (например, потому что вам нужно закрыть свойства и/или состояние компонента) <Mouse> должен расширять React.Component вместо этого.
© 2013–present Facebook Inc.
Licensed under the Creative Commons Attribution 4.0 International Public License.
https://17.reactjs.org/docs/render-props.html