ES6 привнес в экосистему javascript действительно интересную функцию - стрелочные функции.

const foo = (x) => x * x;

Функция стрелки великолепна, потому что ее можно легко использовать для создания закрытия, например, для обработчика событий реакции. Давайте посмотрим на следующее простое приложение:

// ItemRenderer.js
import React from 'react';

const ItemRenderer = ({ onClick, value }) => (
  <div onClick={onClick}>
    {value}
  </div>
);

export default ItemRenderer;
// Group.js
import React from 'react';
import ItemRenderer from './ItemRenderer';
const Group = ({ time, items, log }) => (
  <div>
    <h1>{time}</h1>
    {items.map(
      ({ id, value }) => (
        <ItemRenderer
          key={id}
          value={value}
          onClick={() => { log(`value is: ${value}`); }}
        />
      )
    )}
  </div>
);

export default Group;
// App.js
import React from 'react';
import Group from './Group';

class App extends React.Component {
  constructor() {
    super();
    this.state = {
      time: new Date().toISOString(),
      items: [
        { id: 'a', value: 'Some text' },
        { id: 'b', value: 'Some other text' },
        { id: 'c', value: 'Third text' },
      ],
    };
  }

  componentDidMount() {
    this.timeout = setInterval(
      () => {
        this.setState({
          time: new Date().toISOString(),
        });
      },
      100,
    );
  }

  componentWillUnmount() {
    clearInterval(this.timeout);
  }

  log = (v) => {
    console.log(v);
  };

  render() {
    return (
      <Group
        log={this.log}
        {...this.state}
      />
    );
  }
}

export default App;

Таким образом, приложение должно быть компонентом с временным заголовком и списком элементов. Время должно отображаться один раз в секунду. Чтобы вызвать свойство журнала, переданное из Root с правильными параметрами, мы используем закрытие как параметр:

onClick={() => { log(`value is: ${value}`); }}

У кода, безусловно, есть одна большая проблема с производительностью. Все элементы отображаются при каждом обновлении Группы. Позвольте решить эту проблему, используя чистый усилитель из перекомпоновки, который выполняет поверхностное сравнение свойств перед обновлением компонента:

// ItemRenderer.js
import React from 'react';
import { compose, pure } from 'recompose';

const enhancer = compose(
  pure,
);

const ItemRenderer = ({ onClick, value }) => {
  console.log('re-render!'); // just checking
  return (
    <div onClick={onClick}>
      {value}
    </div>
  );
};

export default enhancer(ItemRenderer);

Теперь убедитесь, что ItemRenderer не отображается повторно. И…

Что это было??? Посмотрим, какие свойства действительно изменились. Для этого подключите reprolog lib и измените код.

// ItemRenderer.js
import React from 'react';
import { compose, pure } from 'recompose';
import { withLogger } from 'reprolog';

const enhancer = compose(
  pure,
  withLogger(),
);

const ItemRenderer = ({ onClick, value }) => (
  <div onClick={onClick}>
    {value}
  </div>
);

export default enhancer(ItemRenderer);

(Полную настройку репролога вы можете увидеть в readme).

И теперь мы видим, что причиной обновления является свойство onClick. Это происходит потому, что при каждой визуализации Группы закрытие воссоздается для каждого отображаемого элемента. Эта крышка выполняет то же самое и имеет идентичную форму. Но это новый экземпляр закрытия. По этой причине это будет то же самое, что и сравнение:

const a = () => {};
const b = () => {};
a === b // false
a == b // false

По этой причине передача встроенных замыканий в качестве стрелочных функций является очень опасным шаблоном (представьте, что ItemRenderer - более тяжелый компонент, и вам нужно отобразить на экране 20 или более из них). Конечно, мы могли бы написать это так:

// ItemRenderer.js
import React from 'react';
import { compose, pure } from 'recompose';
import { withLogger } from 'reprolog';
const enhancer = compose(
  pure,
  withLogger(),
);
const ItemRenderer = ({ onClick, value }) => (
  <div onClick={() => onClick(`value is: ${value}`)}>
    {value}
  </div>
);

export default enhancer(ItemRenderer);
// Group.js
import React from 'react';
import ItemRenderer from './ItemRenderer';

const Group = ({ time, items, log }) => (
  <div>
    <h1>{time}</h1>
    {items.map(
      ({ id, value }) => (
        <ItemRenderer
          key={id}
          value={value}
          onClick={log}
        />
      )
    )}
  </div>
);

export default Group;

Это не будет повторно отображать ItemRenderer при каждом обновлении Group (что решает текущую проблему с производительностью). Но мы не избавились от самого шаблона и внесли больше знаний о поведении обратного вызова в компонент ItemRenderer.

Одно из возможных исправлений этой проблемы - обернуть ItemRenderer средством улучшения, которое добавит эти знания, не изменяя его код. Мы можем использовать усилитель withHandlers от Recompose. Этот усилитель лениво создает обработчики, когда они вызываются, и передает свойства:

// ItemRenderer.js
import React from 'react';
import { compose, pure } from 'recompose';
import { withLogger } from 'reprolog';

const enhancer = compose(
  pure,
  withLogger(),
);

const ItemRenderer = ({ onClick, value }) => (
  <div onClick={onClick}>
    {value}
  </div>
);

export default enhancer(ItemRenderer);
// Group.js
import React from 'react';
import { compose, withHandlers } from 'recompose';
import ItemRenderer from './ItemRenderer';

const itemEnhancer = compose(withHandlers({
  onClick: ({ clickHandler, value }) => () => {
    clickHandler(`value is: ${value}`);
  },
}));

const EnhancedItemRendrer = itemEnhancer(ItemRenderer);

const Group = ({ time, items, log }) => (
  <div>
    <h1>{time}</h1>
    {items.map(({ id, value }) => (
      <EnhancedItemRendrer
        key={id}
        value={value}
        clickHandler={log}
      />
      ))}
  </div>
);

export default Group;

Теперь откроем отладчик и увидим, что никаких обновлений элементов нет:

Обратите внимание: та же проблема возникает со встроенными массивами и встроенными объектами:

{} === {} // false;
[] === [] // false;

В следующей статье мы поговорим о том, как решить эту проблему.

Удачной отладки :)