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.