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;
В следующей статье мы поговорим о том, как решить эту проблему.
Удачной отладки :)