29 Июля вышел React 15.3, и первым пунктом в release-notes значилось добавление поддержки React.PureComponent, который заменяет своего предшественника pure-render-mixin. В этой статье обсудим, почему же этот компонент так важен и где его использовать.

Это один из самых значительных способов оптимизации react-приложений, который можно довольно легко и быстро реализовать. Использование pure-render-mixin дает ощутимый прирост в производительности, так как сокращается количество рендеров в приложении, а значит и react, в свою очередь, производит намного меньше операций.

image

PureComponent изменяет lifecycle-метод shouldComponentUpdate, автоматически проверяя, нужно ли заново отрисовывать компонент. При этом PureComponent будет вызывать рендер только если обнаружит изменения в state или props компонента, а значит во многих компонентах можно менять state без необходимости постоянно писать

if (this.state.someVal !== computedVal) {
    this.setState({someVal: computedVal})
}

В исходниках React при условии, что компонент является «Pure», и проводится такая проверка:

if (this._compositeType === CompositeTypes.PureClass) {
 shouldUpdate = !shallowEqual(prevProps, nextProps) || ! shallowEqual(inst.state, nextState);
}

Использование shallowEqual говорит о том, что происходит неглубокая проверка params и state, так что сравнение не будет происходить по глубоко вложенным объектам, массивам.

Глубокое сравнение — очень затратная операция. Если бы PureComponent каждый раз ее вызывал, то он бы приносил больше вреда, чем пользы. Никто не мешает использовать проверенный shouldComponentUpdate, чтобы вручную определить необходимость нового рендера. Самый простой вариант — прямое сравнение параметров.

shouldComponentUpdate(nextProps, nextState) { 
    return nextProps.user.id === props.user.id; 
}

Также можно использовать immutable данные. Сравнение в таком случае становится очень простым, так как имеющиеся переменные не изменяются, а всегда создаются новые. Библиотеки вроде Immutable.js — наш верный союзник.

Особенности применения


PureComponent экономит нам время и позволяет не писать лишний код, но не является панацеей. Важно знать особенности его применения, иначе полезность теряется. Так как PureComponent предполагает неглубокую проверку, изменения его props и state могут остаться проигнорированными. К примеру, в родительском компоненте есть рендер и обработчик клика:

  handleClick() {
    const items = this.state.items;
    items.push('new-item');
    this.setState({items: items});
  }

  render() {
    return (
      <div>
        <button onClick={this.handleClick} />
        <ItemList items={this.state.items} />
      </div>
    );
  }

Если компонент ItemList сделать PureComponent, то при изменении items после нажатия кнопки ничего не будет происходить. Это случается из-за того, что this.state.items при сравнении будет равен старой версии this.state.items, хотя содержимое его поменялось. Однако это легко исправить, убрав мутации, например вот так:

handleClick() {
  this.setState(prevState => ({
    words: prevState.items.concat(['new-item'])
  }));
}

PureComponent будет всегда заново отрисовывать компоненты, если будет получать ссылки на разные объекты. Это значит, что если мы не хотим терять преимущества PureComponent, следует избегать подобных конструкций:

<Entity values={this.props.values || []}/>

Новый массив, хоть он и пустой, будет всегда заставлять компонент отрисовываться заново.
Избежать такого очень просто, к примеру, с помощью DefaultProps, в котором можно задать изначально пустое состояние передаваемой переменной. Также очень часто можно увидеть такие компоненты:

<CustomInput onChange={e => this.props.update(e.target.value)} />;
При их создании всегда будет создаваться новая функция, а значит и PureComponent будет видеть каждый раз новые данные. Это лечится, например, bind'ом нужной функции в конструкторе компонента.

constructor(props) {
    super(props); this.update = this.update.bind(this); 
}
update(e) {
    this.props.update(e.target.value); 
} 
render() { 
    return <MyInput onChange={this.update} />; 
}

Также любой компонент, который содержит дочерние элементы, созданные в JSX, будет всегда выдавать false на shallowequal проверках.

Важно помнить, что PureComonent пропускает отрисовку не только самого компонента, но и всех его “детей”, так что безопаснее всего применять его в presentational-компонентах, без “детей” и без зависимости от глобального состояния приложения.

Что в итоге


На самом деле переход на PureComponent является довольно простым, если знать ряд особенностей, связанных скорее с самим JS, нежели с React. Во многих компонентах я заменял:

class MyComponent extends Component {…}

на:

class MyComponent extends PureComponent {…} 

… и они продолжали спокойно работать, да еще с увеличенной производительностью.

Так что пробуйте и используйте, компонент очень полезный.

EDIT
Спасибо raveclassic за полезное замечание. В случае, если pure-компонент имеет детей, все дочерние компоненты, зависящие от смены контекста, не будут реагировать на изменения, если в родительском pure-компоненте не будет объявлен contextTypes.
Поделиться с друзьями
-->

Комментарии (10)


  1. MrCheater
    26.12.2016 15:43
    +3

    PureComponent или "чистый компонент" подразумевает, что state внутри не будет, что весьма логично. "Чистый" — значит "без состояния".
    Примеры несколько неудачны.


    1. Vovchikvoin
      26.12.2016 16:37
      +1

      Простите но вы абсолютно не правы. Чистый компонент — это когда render зависит только от state и props. То о чем вы говорите, называется — stateless компонент.


      1. MrCheater
        26.12.2016 16:59
        +2

        Простите, а от чего кроме props и state может зависеть результат работы render?
        Если вы про context, то юзать его можно и в PureComponent, и в Component.


        1. raveclassic
          27.12.2016 11:06
          +1

          Честно говоря, от чего угодно: const Foo = props => Math.random().
          Проблема в том, что команда реакта в свое время позволила себе обозвать технику проверки аргументов в процессе, называемом ими reconciliation, как pure rendering. Они даже дальше пошли — придумали PureComponent.

          Но, первое, о чем навевает мысли слово pure — это чистые функции ФП. По этой аналогии оно и придумано. Пересечение, конечно, есть, но до тех пор, пока в языке отсутствуют декларативные эффекты, те же самые elm/purescript не дадут вам безнаказанно встроить в функцию генератор случайных чисел. Вам придется декларативно указать это в типе, и вот тогда функция продолжить быть чистой, только с эффектом в возвращаемом значении.

          В общем, отсюда и вся путаница.


          1. MrCheater
            27.12.2016 12:40

            Я пал жертвой злых маркетологов и их "модных" названий :-(


  1. raveclassic
    26.12.2016 18:52

    Стоило упомянуть проблемы со сменой контекста при использовании pure-rendering.


    1. Kresent
      26.12.2016 19:11

      Спасибо за полезное замечание.


      1. raveclassic
        27.12.2016 10:37

        shouldComponentUpdate, следовательно и pureComponent, игнорирует изменения в context, проверяя лишь изменения в state и props.
        На самом деле, это тоже не совсем так. Контекст приходит третьим аргументом в shouldComponentUpdate, но тогда и только тогда, когда он явно указан через contextTypes. Проблема в том, что не все компоненты это делают (и это разумно), и, например, промежуточный PureComponent, который рендерит детей, зависящих от контекста (и смены значений в нем), если в нем не объявлены contextTypes, просто проигнорирует апдейт.

        Существует вполне себе решение (и вроде так сделали в react-router) — хранить в контексте неизменяемую ссылку на инстанс эмиттера, и общаться событиями изменения через него. В детях подписываемся и обновляем стейт, можно даже в HOC обернуть.


        1. raveclassic
          27.12.2016 10:53

          Собственно, так же сделано в react-redux — в контексте лежит инстанс стора, а connect на любом уровне подписывается на его изменения и обновляет оборачиваемые компоненты с нужными данными из селекторов.


  1. CrazyOne
    27.12.2016 01:31
    +1

    Как-то мимо обошел изменения в React, просто обновился для галочки. Спасибо за статью!