Immutability in react

Immutability in react

Как использовать immutability в React

В данной статье мы взглянем на UI слой игровой логики Сапера. Здесь есть свои подводные камни, которые мы должны учесть, когда применяем объекты immutable.js к React. Первый – нельзя непосредственно перевести неизменную карту или список в компонент React, как этот:

  1. var data = Immutable.Map();
  2. React.render(MyComponent(data), element);

Это не работает из-за того, что React копирует содержимое объекта для каждого свойства и сливает их с существующими props. Это значит, что React не получит неизменный образец. Также она не получит данные, что содержатся в карте, потому что они не представлены как свойства объекта – нужно обратиться к данным с помощью метода get().

Решение довольно простое:

  1. var data = Immutable.Map();
  2. React.render(MyComponent({data: data}), element);

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

  1. function createComponent(def) {
  2.  var component = React.createFactory(React.createClass(def));
  3.  return function (data) {
  4.   return component({data: data});
  5.  };
  6. }

С этим компонентом мы можем забыть об обертке пока нам не потребуется запустить функцию render() (где нам надо будет обратиться к ней, как к this.props.data). Компоненты будут только определять render(), так что наша обертка может сделать еще больше для нас.

  1. function createComponent(render) {
  2.  var component = React.createFactory(React.createClass({
  3.   render: function () {
  4.    return render(this.props.data);
  5.   }
  6.  }));
  7.  
  8.  return function (data) {
  9.   return component({data: data});
  10.  };
  11. }

С этим, определить и использовать компоненты, работающие с неизменными данными, очень легко:

  1. var div = React.DOM.div;
  2.  
  3. var Tile = createComponent(function (tile) {
  4.  if (tile.get('isRevealed')) {
  5.   return div({className: 'tile' + (tile.get('isMine') ? ' mine' : '')},
  6.     tile.get('threatCount') > 0 ? tile.get('threatCount') : '');
  7.  }
  8.  
  9.  return div({
  10.   className: 'tile'
  11. } , div({className: 'lid'}, ''));
  12. });

Когда клетка раскрыта, рендерим мину, если такая имеется; иначе рендерим число соседних мин, кроме случая, когда оно равно 0. Если мина не обнаружена, мы рендерим ее с лидом, который CSS сделает видным, как клетку, по которой можно кликнуть.
Остальные компоненты React так же просты. Осталась только одна загвоздка. Компоненты React могут принимать массив дочерних компонентов. Нужно быть уверенным, что неизменные списки конвертированы в массивы перед передачей их в React:

  1. var Row = createComponent(function (tiles) {
  2.  return div({className: 'row'}, tiles.map(Tile).toJS());
  3. });

Вызов map() в неизменном списке создает новый неизменный список, а toJS() возвращает образ массива, с которым может работать React. Эти и остальные компоненты UI можно увидеть полностью на CodePen, где также можно поиграть.

Ускоряем все

В предыдущей части мы упомянули, что отслеживание изменений может быть кардинально улучшено, потому что мы можем упростить алгоритм определения различий в таких библиотеках, как React. Когда загружаешь в React новые данные, он запускает функцию shouldComponentUpdate() для всех компонентов. Когда эта функция выдаст false, React не будет различать этот компонент с существующей версией, а значит библиотека не прорендерит элементы, которые составляют этот компонент. Это сэкономит много работы и может привести к большому прорыву в процессе. 

Давайте обсудим нашу игру. Если раскрыть клетку, целая игра рендерится по-новому. Однако, благодаря нашей неизменной модели данных, все клетки, которые не изменяются, будут иметь такие же ссылки. Они не требуют повторного рендеринга, потому что с неизменными данными, такие же ссылки означают отсутствие изменений. Если уведомить React об этой детали, мы можем усовершенствовать нашу обертку так:

  1. function createComponent(render) {
  2.  var component = React.createFactory(React.createClass({
  3.   shouldComponentUpdate: function (newProps, newState) {
  4.    // Simplified, this app only uses props
  5.    return newProps.data !== this.props.data;
  6.   },
  7.  
  8.   render: function() {
  9.    return render.call(this, this.props.data);
  10.   }
  11.  }));
  12.  
  13.  return function (data) {
  14.   return component({data: data});
  15.  };
  16. }

Этого простого кода достаточно, чтобы сделать наше приложение, которое было существенно медленнее, чем базированное на изменчивых данных, вдвое быстрее. Вот как важно проделать эффективное отслеживание изменений. Улучшенная версия этой статьи также доступна на CodePen. Если хотите посмотреть поближе на цифры, есть также GitHub repository, который включает сырые показатели и множество имплементаций.

Упрощение

Самым полезным в постоянных данных есть то, как они снижают вероятность случайной сложности. Изменчивые данные по существу сложнее, чем неизменные, потому что они запутывают состояние и время. В изменчивых данных, время – это встроенный фактор. Фактически, обращение к двум разным точкам во времени, вероятно, даст вам два разных значения. Неизменные данные не имеют такого свойства. Если извлечь значение с неизменных данных в двух точках во времени, получиться точно одно значение. Это заставляет нас серьёзнее взглянуть на то, как данные изменяются со временем. 

С неизменными данными такие виды свойств, которые слишком сложные или вообще нереальные с изменчивыми данными, стают элементарными в имплементации.  Примером такой особенности есть опция отмены. Если состояние приложение может быть представлено неизменными значениями, осуществление отмены – это повод держать список состояний приложения и создать кнопку, чтобы вернуть приложение в прежнее состояние. 

Давайте применим «отмену» к нашему Саперу. Когда пользователь кликает на клетку, мы имеем новый объект игры для рендеринга. Чтобы обеспечить эту функцию, мы сохраним старую. Код, который исполняет эту функцию, подан ниже:

  1. var newGame = revealTile(game, tile);
  2.  
  3. if (newGame !== game){
  4.  gameHistory = gameHistory.push(newGame);
  5.  game = newGame;
  6. }
  7.  
  8. render();

А это уже кнопка «отмена»:

  1. var UndoButton = createComponent(function () {
  2.  return React.DOM.button({
  3.   onClick: function () {
  4.    channel.emit('undo');
  5.   }
  6.  }, 'Undo');
  7. });

Объект channel - это источатель событий (Event Emitter), так что нам потребуется код, который отзовется на это событие. Пока мы еще не очистили историю, мы удаляем последнюю версию, восстанавливаем предпоследнюю, как текущее состояние, и повторно рендерим игру. Это делается с помощью следующего кода:

  1. channel.on('undo', function ()
  2.  if (history.size > 1) {
  3.   gameHistory = gameHistory.pop();
  4.   game = gameHistory.last();
  5.   render();
  6.  }
  7. });

Очень просто, не так ли? Вы можете представить, как сделать что-то подобное в приложениях, состояние которых полностью состоит с изменчивых объектов? Играйте Сапера с отменой (хотя это и жульничество), обращаясь к CodePen, который вы увидите на демо ниже.

Помните, мы упомянули структурное деление в предыдущей части? Структурное деление значит, что хранение двух почти идентичных версий состояния приложения на самом деле не сохранит две копии одного и того же состояния. Это делает невозможным тот факт, что неизменные данные стабильны. Если вам нужно решить проблему с отменой, нужно особым образом сохранить изменения, чтобы они совпали с неизменным решением с точки зрения использования памяти. С другой стороны, вы дойдете до сохранения некоторых полномасштабных копий, которые плохо соизмеряются.

Снимки файловой системы (снапшоты) приложения

Вы, может быть, думаете, что во многих приложениях мало пользы от функции «отмена». Но есть еще случаи, когда это довольно интересно. Например, если полное состояние приложения сохранено в одном неизменном значении, можно так же легко добавить к этому приложению снапшоты. Для чего это нужно? Во-первых, чтобы сохранить текущее состояние в произвольном комплексном UI. И во-вторых, чтобы находить и устранять неполадки.

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

 

Выводы

Мы загрузили исходный код к статье об установлении Сапера на GitHub, где вы сможете погрузиться в детали. Вы найдете там имплементацию с использованием так изменчивых, так и постоянных структур данных, вместе с инструкциями, как вести себя с простыми показателями. 

Дерзайте и устанавливайте immutability в свои приложения!

Автор статьи - Christian Johansen
Ссылка на оригинал