3.12 Паттерн: свойство render

Термин «свойство render» (в оригинале «render prop») относится к простой методике совместного использования кода между компонентами React, принимающими свойство prop, значение которого является функцией.

Компонент со свойством render принимает функцию, которая возвращает элемент React, и вызывает её вместо реализации своей собственной логики отрисовки.


Код
    
  <DataProvider render={data => (
    <h1>Hello {data.target}</h1>
  )}/>
  

Библиотеками, использующими такой подход, являются React Router и Downshift .

В этом разделе мы обсудим, почему полезно использовать свойство для отрисовки.


3.12.1 Используйте «свойство render» для сквозной функциональности

Компоненты являются основным элементом повторного использования кода в 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 дает возможность применить любой паттерн.


3.12.2 Использование свойств, отличных от "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>
  

Вы можете увидеть этот подход в react-motion API.

Поскольку этот метод немного необычен, вы при разработке API, вероятно, захотите явно указать, что свойство children должно быть функцией в ваших propTypes, как здесь:


Код
    
  Mouse.propTypes = {
    children: PropTypes.func.isRequired
  };
  


3.12.3 Будьте внимательны, когда используете паттерн «свойство render» с React.PureComponent

Использование свойства 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.