Книга: React.js. Быстрый старт
Назад: 7. Проверка качества кода, соответствия типов, тестирование, повтор
На главную: Предисловие

8. Flux

Последняя глава этой книги посвящена Flux — технологии, являющейся альтернативным способом управления обменом данными между компонентами и средством управления всеми потоками данных в вашем приложении. До сих пор вы видели, как обмен данными выполняется путем передачи свойств от родительского компонента дочернему с последующим отслеживанием изменений, происходящих в дочернем компоненте (например, с помощью метода onDataChange). Но, передавая свойства таким способом, иногда в конечном итоге можно получить компонент, принимающий слишком много свойств. Это затрудняет тестирование такого компонента и проверку того, что все возможные комбинации и перестановки свойств работают в соответствии с вашими ожиданиями.

Кроме того, временами возникают сценарии, где для свойств нужен «сквозной канал» от родительского компонента к дочернему, а затем и к внучатому, правнучатому и т.д. Появляется тенденция повторяемости (что плохо само по себе), которая вызывает путаницу и повышает психическую нагрузку на человека, читающего код (слишком многое приходится одновременно держать в памяти).

Flux предлагает способ преодоления этих трудностей и сохранения комфортного психического состояния, не утрачивая при этом возможности управления потоком данных в вашем приложении. Flux не является библиотекой кодов, скорее это замысел, каса­ющийся способа организации (создания архитектуры) данных приложения. Все же в большинстве случаев важны именно данные. Пользователи заходят в ваше приложение для работы со своими деньгами, электронной почтой, фотографиями или чем-нибудь еще. Даже если пользовательский интерфейс не отличается особой элегантностью, они могут с этим смириться. Но они никогда не должны попадать в непонятные ситуации относительно состояния данных («Я отправил или не отправил эти 30 долларов?»).

Идеи, присущие Flux-технологии, воплощены во многих реализациях с открытым кодом. Вместо всеобъемлющего охвата имеющихся вариантов в этой главе рассматривается подход типа «сделай сам». Как только вы уясните замысел (и приобретете уверенность в преимуществах его реализации), можно будет продолжить исследование доступных вариантов или приступить к разработке собственного решения.

Основной замысел

Замысел основан на том, что самое важное в вашем приложении — это данные. Они содержатся в хранилище (store). React-компоненты, или представление (view), считывают данные из хранилища и выводят их на экран. Затем дает о себе знать пользователь приложения, выполняющий действие (action), щелкая, к примеру, на кнопке. Действие заставляет обновлять данные в хранилище, что оказывает влияние на представление. И этот цикл повторяется раз за разом (рис. 8.1). Поток данных идет в одном направлении (однонаправленно), что существенно упрощает отслеживание, осмысление происходящего и отладку.

163319.png 

Рис. 8.1. Однонаправленный поток данных

Существует множество вариантов и развитий этой идеи, где используется большее количество действий, несколько хранилищ и работа диспетчера (dispatcher), но прежде чем пере­ходить к пространным объяснениям, посмотрим на программный код.

Иной взгляд на Whinepad

У приложения Whinepad есть высокоуровневый React-компонент <Whinepad>, созданный с применением следующего кода:

<Whinepad

  schema={schema}

  initialData={data} />

В свою очередь, компонент <Whinepad> формирует компонент <Excel>:

<Excel

  schema={this.props.schema}

  initialData={this.state.data}

  onDataChange={this._onExcelDataChange.bind(this)} />

Сначала от <Whinepad> к <Excel> в неизменном виде передается (проходит по каналу) schema, то есть описание данных, с которыми работает приложение. (А затем точно так же осуществляется ее передача в адрес компонента <Form>.) Здесь явно прослеживаются некая повторяемость и шаблонность. А что, если потребуется прогнать по каналу несколько подобных свойств? Вскоре внешняя сторона ваших компонентов станет слишком большой, что явно не принесет особой пользы.

162090.png

Понятие «внешняя сторона» в данном контексте означает свойства, получаемые компонентом. Оно используется в качестве синонима API-интерфейса или сигнатуры функции. Как и все­гда в программировании, внешнюю сторону лучше сводить к минимуму. Функцию, получающую десять аргументов, намного труднее использовать, доводить до нужного состояния и тестировать, чем функцию, получающую всего два аргумента или вообще не получающую никаких аргументов.

Схема schema просто передается в неизменном виде, но, похоже, на данные это не распространяется. <Whinepad> получает свойство initialData, но затем передает компоненту <Excel> его версию (this.state.data, а не this.props.initialData). И тут возникают вопросы: чем новые данные отличаются от оригинала? И где находится единственный источник истины, когда речь заходит о самых последних данных?

В реализации, которая была показана в предыдущей главе, самые актуальные данные содержались в <Whinepad> и все работало без нареканий. Но не вполне понятно, почему компонент пользовательского интерфейса (а React занимается созданием пользовательского интерфейса) должен быть хранителем источника истины.

Для выполнения этой миссии введем хранилище.

Хранилище

Сначала скопируем весь созданный до сих пор код:

$ cd ~/reactbook

$ cp -r whinepad2 whinepad3

$ cd whinepad3

$ npm run watch

Затем создадим новый каталог для хранения Flux-модулей (чтобы отделить их от компонентов пользовательского интерфейса React), которых будет всего два — хранилище (store) и действия (actions):

$ mkdir js/source/flux

$ touch js/source/flux/CRUDStore.js

$ touch js/source/flux/CRUDActions.js

В архитектуре Flux может быть несколько хранилищ (например, одно для пользовательских данных, другое — для настроек приложения и т.д.), но наше внимание будет приковано только к одному — CRUD-хранилищу, целиком предназначенному для списка записей; в данном примере речь пойдет о записях марок вина и вашего мнения о них.

Для CRUDStore технология React не пригодится, и код хранилища может быть реализован в виде простого объекта JavaScript:

/* @flow */

 

let data;

let schema;

 

const CRUDStore = {

 

getData(): Array<Object> {

  return data;

},

 

getSchema(): Array<Object> {

  return schema;

},

};

 

export default CRUDStore

Как видите, хранилище обслуживает единый источник истины в виде локального модуля данных переменных и схемы и охотно возвращает эти данные всем, кто ими интересуется. Хранилище также допускает обновление данных (за исключением схемы, остающейся неизменной в течение всего жизненного цикла приложения):

setData(newData: Array<Object>, commit: boolean = true) {

  data = newData;

  if (commit && 'localStorage' in window) {

    localStorage.setItem('data', JSON.stringify(newData));

  }

  emitter.emit('change');

},

Здесь, кроме обновления локальной переменной data, обновляется и постоянное хранилище, которое в данном случае представлено объектом localStorage, но оно может быть также размещено на сервере, получающем XHR-запрос. Это обновление происходит только при отправке данных, поскольку обновлять постоянное хранилище при каждом изменении не нужно. Например, при поиске хочется, чтобы его результатом были самые последние данные, но не нужно, чтобы результаты попадали на постоянное хранение. Что если после вызова setData() случится сбой электропитания и будут утеряны все данные, кроме результатов поиска?

И наконец, здесь видно, что выдается событие 'change' (изменение). (На этом моменте мы еще остановимся.)

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

getCount(): number {

  return data.length;

},

 

getRecord(recordId: number): ?Object {

  return recordId in data ? data[recordId] : null;

},

Чтобы запустить приложение, нужно инициализировать хранилище. Прежде эта работа выполнялась в app.js, но теперь она вполне резонно возлагается на хранилище, чтобы вся работа с данными велась в одном месте:

init(initialSchema: Array<Object>) {

  schema = initialSchema;

  const storage = 'localStorage' in window

    ? localStorage.getItem('data')

    : null;

  if (!storage) {

    data = [{}];

    schema.forEach(item => data[0][item.id] = item.sample);

  } else {

    data = JSON.parse(storage);

  }

},

А в app.js теперь осуществляется начальная загрузка приложения:

// ...

import CRUDStore from './flux/CRUDStore';

import Whinepad from './components/Whinepad';

import schema from './schema';

 

CRUDStore.init(schema);

 

ReactDOM.render(

  <div>

    {/* код JSX */}

    <Whinepad />

  {/* ... */}

);

Как видите, после инициализации хранилища компоненту <Whine­pad> не нужно получать никаких свойств. Необходимые ему данные доступны с помощью вызова метода CRUDStore.getData(), а описание данных берется из вызова метода CRUDStore.getSchema().

162095.png

Возможно, вас удивляет, почему хранилище считывает данные отдельно, а затем полагается на схему, передаваемую извне. Разумеется, можно заставить хранилище импортировать модуль schema. Но, наверное, вполне разумно позволить приложению решать, откуда будет браться схема. Будет ли она определяться пользователем, или поставляться в виде модуля, или присутствовать в виде жестко заданного кода?

События хранилища

Вы помните часть кода emitter.emit('change');, выполняемую при обновлении хранилищем своих данных? Это способ информирования хранилищем любых заинтересованных модулей пользовательского интерфейса о том, что данные изменились и они теперь могут выполнить собственное обновление, считывая свежие данные из хранилища. А как выполняется эта выдача события?

Реализовать модель подписки на событие можно множеством способов, по сути, все сводится к составлению списка заинтересованных сторон (подписчиков) и к необходимости «публикации» о наступлении события путем запуска на выполнение функции обратного вызова каждого подписчика (той функции, которую подписчик предоставляет при оформлении подписки).

Чтобы не утруждать себя созданием собственного кода, для формирования частей кода, занимающихся подпиской на события, воспользуемся небольшой открытой библиотекой под названием fbemitter:

$ npm i --save-dev fbemitter

Обновим файл .flowconfig:

[ignore]

.*/fbemitter/node_modules/.*

# и так далее ...

 

[include]

node_modules/classnames

node_modules/fbemitter

# и так далее ...

Импортирование и инициализация источника событий происходят в верхней части модуля хранилища:

/* @flow */

 

import {EventEmitter} from 'fbemitter';

 

let data;

let schema;

const emitter = new EventEmitter();

 

const CRUDStore = {

  // ...

};

export default CRUDStore

У источника событий две задачи:

сбор подписок;

уведомление подписчиков (как уже было показано при использовании emitter.emit('change') в setData()).

Предоставить коллекции подписчиков можно с использованием метода в хранилище, поэтому вызывающему коду вдаваться в какие-либо подробности не нужно:

const CRUDStore = {

  // ...

  addListener(eventType: string, fn: Function) {

      emitter.addListener(eventType, fn);

  },

  // ...

};

Вот теперь хранилище CRUDStore стало функционально завершенным.

Использование хранилища  в <Whinepad>

Компонент <Whinepad> при использовании Flux-технологии существенно упростился. Главным образом это достигнуто за счет перекладывания функциональных обязанностей на модуль CRUDActions (который вскоре будет показан), но помощь оказана и со стороны модуля CRUDStore. Работать со свойством this.state.data больше нет необходимости. Оно было востребовано только лишь для его передачи компоненту <Excel>. Но теперь <Excel> может обратиться за данными в хранилище. Фактически компоненту <Whinepad> вообще не нужно работать с хранилищем. Но добавим еще одну функцию, для которой требуется обращение к хранилищу. Эта функция предназначена для отображения общего количества записей в поле поиска (рис. 8.2).

08_02.tif 

Рис. 8.2. Количество записей в поле поиска

Ранее метод constructor() компонента <Whinepad> устанавливал состояние следующим образом:

this.state = {

  data: props.initialData,

  addnew: false,

};

Теперь вам не требуется свойство data, но возникла потребность в свойстве count, которое следует инициализировать путем считывания данных из хранилища:

/* @flow */

 

// ...

import CRUDStore from '../flux/CRUDStore';

// ...

 

class Whinepad extends Component {

  constructor() {

    super();

    this.state = {

      addnew: false,

      count: CRUDStore.getCount(),

    };

  }

  /* ... */

}

 

export default Whinepad

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

constructor() {

  super();

  this.state = {

    addnew: false,

    count: CRUDStore.getCount(),

  };

 

  CRUDStore.addListener('change', () => {

    this.setState({

      count: CRUDStore.getCount(),

    })

  });

}

И на этом все обязательное взаимодействие с хранилищем заканчивается. При любом производимом каким-либо образом обновлении данных в хранилище (и вызове в CRUDStore метода setData()) хранилище выдает событие 'change' (изменение). Компонент <Whinepad> отслеживает выдачу этого события и обновляет свое состояние. Как вам уже известно, установка состоя­ния приводит к повторному отображению компонента, то есть снова вызывается метод render(). Его задача, как и обычно, заключается в создании пользовательского интерфейса на основе состояния и значений свойств:

render() {

  return (

    {/* ... */}

    <input

      placeholder={this.state.count === 1

        ? 'Search 1 record...'

        : `Search ${this.state.count} records...`

      }

    />

    {/* ... */}

  );

}

Есть смысл также реализовать в <Whinepad> метод shouldCompo­nentUpdate(). В данных могут осуществляться изменения, не оказывающие влияния на общее количество записей (например, редактирование записи или редактирование отдельного поля в записи). В таком случае компонент не требует повторного отображения:

shouldComponentUpdate(newProps: Object, newState: State): boolean {

  return (

    newState.addnew !== this.state.addnew ||

    newState.count !== this.state.count

  );

}

И наконец, компоненту <Whinepad> больше не нужно передавать свойства данных и схемы компоненту <Excel>. Не нужно ему и подписываться на метод onDataChange, поскольку все изменения поступают в виде события 'change' от хранилища. Теперь соответствующие части метода render() в компоненте <Whinepad> получили следующий вид:

render() {

  return (

    {/* ... */}

    <div className="WhinepadDatagrid">

        <Excel />

    </div>

    {/* ... */}

  );

}

Использование хранилища в <Excel>

Компонент <Excel> точно так же, как и <Whinepad>, больше не нуждается в свойствах. Конструктор может считать из хранилища схему schema и сохранить ее в this.schema. Разница между хранением схемы в this.state.schema и в this.schema только в том, что состояние предполагает некую степень изменений, а схема является константой.

Что касается данных, исходное значение this.state.data считывается из хранилища и его получение в виде свойства больше уже не осуществляется.

И наконец, конструктор подписывается на событие хранилища 'change', поэтому состояние может быть обновлено самыми свежими данными (что вызовет запуск повторного отобра­жения):

constructor() {

  super();

  this.state = {

    data: CRUDStore.getData(),

    sortby: null, // schema.id

    descending: false,

    edit: null, // {row index, schema.id},

    dialog: null, // {type, idx}

  };

  this.schema = CRUDStore.getSchema();

  CRUDStore.addListener('change', () => {

    this.setState({

      data: CRUDStore.getData(),

    })

  });

}

И это все, что нужно сделать в компоненте <Excel>, чтобы воспользоваться хранилищем. Метод render() по-прежнему считывает данные из this.state для точно такого же их представления, что и раньше.

Необходимость копирования данных из хранилища в this.state может вызвать удивление. А нельзя ли сделать так, чтобы метод render() получал доступ к хранилищу и выполнял чтение непосредственно из него? Конечно, можно. Но тогда компонент утратит свою «чистоту». Следует помнить, что чистый компонент визуализации выполняет отображение только на основе име­ющихся у него свойств и состояния. Любые вызовы функций в render() выглядят подозрительно, ведь никогда не известно, какого рода значения будут получены из внешнего вызова. Возникают сложности в отладке, и приложение становится менее предсказуемым («Почему показано число 2, когда в состоянии содержится 1? А, вот в чем дело, оказывается, в render() есть вызов функции»).

Использование хранилища в <Form>

Компонент формы также получает схему (в виде свойства полей fields) и свойство исходных значений defaultValues для предварительного заполнения формы или отображения версии, предназначенной только для чтения. И то и другое отныне находится в хранилище. Теперь форма может взять свойство recordId и найти нужные данные в хранилище:

/* @flow */

 

import CRUDStore from '../flux/CRUDStore';

 

// ...

 

type Props = {

  readonly?: boolean,

  recordId: ?number,

};

 

class Form extends Component {

  fields: Array<Object>;

  initialData: ?Object;

 

  constructor(props: Props) {

    super(props);

    this.fields = CRUDStore.getSchema();

    if ('recordId' in this.props) {

      this.initialData =

        CRUDStore.getRecord(this.props.recordId);

    }

  }

 

  // ...

}

 

export default Form

Форма не подписывается на событие хранилища 'change', поскольку не ожидает изменений в ходе редактирования в ней данных. Но такое развитие событий нельзя исключать; скажем, другой пользователь редактирует данные в то же самое время или тот же самый пользователь открыл то же самое приложение в двух вкладках и редактирует данные в обоих экземплярах приложения. В таком случае можно отслеживать изменения данных и предупреждать пользователя, работающего в другом экземпляре приложения.

Где провести границу?

Где провести границу между использованием Flux-хранилища и применением свойств в реализации, предшествовавшей Flux? Хранилище представляет собой удобный универсальный магазин, удовлетворяющий все потребности в данных. Оно избавляет вас от необходимости раздачи свойств. Но оно снижает возможность многократного использования компонентов. Теперь уже нельзя повторно использовать компонент Excel в совершенно другом контексте, поскольку в нем жестко закодирован поиск данных в хранилище CRUDStore. Но при условии, что новый контекст похож на эту используемую CRUD-технологию (что весьма вероятно, иначе зачем бы вам понадобилась редактируемая таблица данных?), вы также можете воспользоваться и хранилищем. Однако не забудьте, что приложение может применять столько хранилищ, сколько ему угодно.

Низкоуровневые компоненты вроде кнопок и форм ввода лучше не связывать с хранилищем. Они вполне могут справиться со своей задачей, пользуясь исключительно свойствами. Любые типы компонентов, находящиеся между двумя экстремальными позициями, — простые кнопки (такие как <Button>) и общие родительские компоненты (такие как <Whinepad>) — попадают в зону неопределенности, и решение остается за вами. Должна ли форма прикрепляться к хранилищу типа CRUDstore, как показано ранее, или должна обходиться без него и сохранять возможность повсеместного повторного использования? Выберите наиболее рациональные решения с точки зрения поставленной задачи и перспектив повторного использования создаваемого в данный момент компонента.

Действия

Действия (actions) — способ изменения данных в хранилище (store). Когда пользователи взаимодействуют с представлением (view), они совершают действия, обновляющие данные в хранилище, которое отправляет событие заинтересованным в нем представлениям.

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

/* @flow */

 

import CRUDStore from './CRUDStore';

 

const CRUDActions = {

  /* методы */

};

 

export default CRUDActions

CRUD-действия

Какие методы должны быть реализованы в модуле CRUDActions? Обычно предполагается, что создание — create(), удаление — delete(), обновление — update()… Вот только в этом приложении можно обновить всю запись или обновить отдельное поле, поэтому реализуем методы updateRecord() и updateField():

/* @flow */

/* ... */

const CRUDActions = {

 

  create(newRecord: Object) {

    let data = CRUDStore.getData();

    data.unshift(newRecord);

    CRUDStore.setData(data);

  },

 

  delete(recordId: number) {

    let data = CRUDStore.getData();

    data.splice(recordId, 1);

    CRUDStore.setData(data);

  },

 

  updateRecord(recordId: number, newRecord: Object) {

    let data = CRUDStore.getData();

    data[recordId] = newRecord;

    CRUDStore.setData(data);

  },

 

  updateField(recordId: number, key: string,

    value: string|number) {

    let data = CRUDStore.getData();

    data[recordId][key] = value;

    CRUDStore.setData(data);

  },

 

  /* ... */

};

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

162100.png

Составляющая R акронима CRUD вам не нужна, поскольку соответствующее действие предоставляется хранилищем.

Поиск и сортировка

В предыдущей реализации компонент <Whinepad> отвечал за поиск данных. Причина заключалась в том, что поле поиска находилось в принадлежащем компоненту методе render(). Но вообще-то поиск должен быть где-нибудь ближе к данным.

Точно так же сортировка была частью компонента <Excel>, поскольку для ее выполнения использовались обработчики события onclick, выдаваемого по щелчку на заголовках таблицы. Но, опять-таки, сортировку лучше выполнять ближе к тому месту, где содержатся данные.

Можно порассуждать, где именно должны осуществляться поиск и сортировка — в действиях или хранилище? Похоже, что подходят оба места. Но в данной реализации оставим хранилище в покое, чтобы оно могло только получать и устанавливать данные, а также отвечать за выдачу событий. Манипуляция данными осуществляется в действиях, поэтому перенесем сортировку и поиск из компонентов пользовательского интерфейса в модуль CRUDActions:

/* @flow */

/* ... */

const CRUDActions = {

 

  /* ... CRUD-методы ... */

 

  _preSearchData: null,

 

  startSearching() {

    this._preSearchData = CRUDStore.getData();

  },

 

  search(e: Event) {

    const target = ((e.target: any): HTMLInputElement);

    const needle: string = target.value.toLowerCase();

    if (!needle) {

      CRUDStore.setData(this._preSearchData);

      return;

    }

    const fields = CRUDStore.getSchema().map(item =>

      item.id);

    if (!this._preSearchData) {

      return;

    }

    const searchdata = this._preSearchData.filter(row => {

      for (let f = 0; f < fields.length; f++) {

        if (row[fields[f]].toString().toLowerCase().

          171027.pngindexOf(needle) > -1) {

          return true;

        }

      }

      return false;

      });

      CRUDStore.setData(searchdata, /* commit */ false);

    },

 

    _sortCallback(

      a: (string|number), b: (string|number),

        descending: boolean

    ): number {

    let res: number = 0;

    if (typeof a === 'number' && typeof b === 'number') {

      res = a - b;

    } else {

      res = String(a).localeCompare(String(b));

    }

    return descending ? -1 * res : res;

  },

 

  sort(key: string, descending: boolean) {

    CRUDStore.setData(CRUDStore.getData().sort(

      (a, b) => this._sortCallback(a[key], b[key], descending)

    ));

  },

};

И с этим кодом модуль CRUDActions можно считать функционально законченным. Посмотрим, как он используется компонентами <Whinepad> и <Excel>.

162105.png

Можно не согласиться с тем, что данная часть функции sort() принадлежит CRUDActions:

search(e: Event) {

  const target = ((e.target: any): HTMLInputElement);

  const needle: string = target.value.toLowerCase();

  /* ... */

}

Возможно, модуль действия не должен ничего знать о пользовательском интерфейсе, и «правильная» сигнатура должна быть больше похожа на следующую:

search(needle: string) {

  /* ... */

}

Это вполне разумно, можно пойти и по этому пути. Только вот компоненту <Whinepad> хлопот прибавится, к тому же потребуется более объемный код, чем <input onChange="CRUDActi­ons.search">.

Использование действий в <Whinepad>

Посмотрим, как теперь выглядит компонент <Whinepad> после перехода на Flux-действия. Во-первых, в него, конечно же, должен быть включен модуль действий:

/* @flow */

 

/* ... */

import CRUDActions from '../flux/CRUDActions';

/* ... */

 

class Whinepad extends Component {/* ... */}

 

export default Whinepad

Вспомним, что класс Whinepad отвечает за добавление новых записей и за поиск существующих записей (рис. 8.3).

08_03.tif 

Рис. 8.3. Область ответственности Whinepad  за работу с данными

Что касается добавления новых записей, то ранее Whinepad отвечал за работу со своим собственным свойством this.state.data:

_addNew(action: string) {

  if (action === 'dismiss') {

    this.setState({addnew: false});

  } else {

    let data = Array.from(this.state.data);

    data.unshift(this.refs.form.getData());

    this.setState({

      addnew: false,

      data: data,

    });

    this._commitToStorage(data);

  }

}

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

_addNew(action: string) {

  this.setState({addnew: false});

  if (action === 'confirm') {

    CRUDActions.create(this.refs.form.getData());

  }

}

Нет ни состояния, которое нужно обслуживать, ни данных, с которыми нужно работать. При каком-либо действии пользователя его надо просто делегировать — и оно пойдет по однонаправленному потоку данных.

Аналогично обстоят дела и с поиском. Если прежде он выполнялся в отношении собственного свойства компонента this.state.data, то теперь все сводится к наличию следующего кода:

<input

  placeholder={this.state.count === 1

    ? 'Search 1 record...'

    : 'Search ${this.state.count} records...'

  }

  onChange={CRUDActions.search.bind(CRUDActions)}

  onFocus={CRUDActions.startSearching.bind(CRUDActions)} />

Использование действий в компоненте <Excel>

Компонент Excel является инициатором и получателем сор­тировки, удаления и обновления, предоставляемых модулем CRUDActions. Если помните, раньше удаление выглядело следу­ющим образом:

_deleteConfirmationClick(action: string) {

  if (action === 'dismiss') {

    this._closeDialog();

    return;

  }

  const index = this.state.dialog ?

    this.state.dialog.idx : null;

  invariant(typeof index === 'number',

    'Unexpected dialog state');

  let data = Array.from(this.state.data);

  data.splice(index, 1);

  this.setState({

    dialog: null,

    data: data,

  });

  this._fireDataChange(data);

}

А сейчас оно превратилось в следующий код:

_deleteConfirmationClick(action: string) {

  this.setState({dialog: null});

  if (action === 'dismiss') {

    return;

  }

  const index = this.state.dialog && this.state.dialog.idx;

  invariant(typeof index === 'number',

    'Unexpected dialog state');

  CRUDActions.delete(index);

}

Теперь уже не выдается событие изменения данных, поскольку отслеживание событий, происходящих в компоненте Excel, никем не ведется, всеобщие интересы нацелились на хранилище. И больше не нужно работать со свойством this.state.data. Вместо этого вся работа возложена на модуль действий, а обновление происходит, когда модуль хранилища выдает событие.

Аналогично обстоят дела с сортировкой и обновлением записей. Вся работа с данными превратилась в отдельные вызовы методов модуля CRUDActions:

/* @flow */

 

/* ... */

import CRUDActions from '../flux-imm/CRUDActions';

/* ... */

 

class Excel extends Component {

  /* ... */

  _sort(key: string) {

    const descending = this.state.sortby ===

      key && !this.state.descending;

    CRUDActions.sort(key, descending);

    this.setState({

      sortby: key,

      descending: descending,

    });

  }

 

  _save(e: Event) {

    e.preventDefault();

    invariant(this.state.edit, 'Messed up edit state');

    CRUDActions.updateField(

      this.state.edit.row,

      this.state.edit.key,

      this.refs.input.getValue()

    );

    this.setState({

      edit: null,

    });

  }

 

  _saveDataDialog(action: string) {

    this.setState({dialog: null});

    if (action === 'dismiss') {

      return;

    }

    const index = this.state.dialog && this.state.dialog.idx;

    invariant(typeof index === 'number',

      'Unexpected dialog state');

    CRUDActions.updateRecord(index, this.refs.form.getData());

  }

 

  /* ... */

};

 

export default Excel

162111.png

Полностью преобразованная версия приложения Whinepad, использующего Flux-технологию, доступна в хранилище кода, сопровождающем книгу.

И еще немного о Flux

Вот и все. Приложение теперь перешло на использование архитектуры Flux (некой разновидности ее самодельной версии). Есть представление (view), отправляющее данные о действиях пользователя модулю действий (actions), который обновляет данные в единственном хранилище (store), выдающем события. Затем представление отслеживает события хранилища и обновляется. Получается замкнутый цикл.

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

Действия могут отправляться не только представлением (view) (рис. 8.4), но и с сервера. Возможно, какие-то данные устарели. Может быть, другие пользователи меняли данные и приложение определило это после синхронизации с сервером. Или прошло время — и должны быть предприняты какие-то действия (настал срок выкупить забронированные билеты, сессия просрочена, и нужно начинать все сначала!).

163359.png 

Рис. 8.4. Дополнительные действия

Когда возникает ситуация использования нескольких источников действий, на первый план выходит весьма конструктивная идея единого диспетчера (dispatcher) (рис. 8.5). Этот диспетчер отвечает за передачу всех этих действий в хранилище (store) или хранилища.

168405.png 

Рис. 8.5. Диспетчер

А в более интересных приложениях приходится иметь дело с различными действиями, поступающими из пользовательского интерфейса, от сервера или еще откуда-нибудь, и с несколькими хранилищами, каждое из которых отвечает за свои собственные данные (рис. 8.6).

163379.png 

Рис. 8.6. Усложненный, но по-прежнему однонаправленный поток данных

Существует множество решений с открытым кодом, позволя­ющих реализовать Flux-архитектуру. Но вы всегда можете для начала выбрать небольшое по объему приложение и наращивать его по мере увеличения объема данных либо путем развития своего собственного решения, либо выбрав одно из предлагаемых решений с открытым кодом, о возможностях расширения которого вам уже известно.

Библиотека immutable

Завершим книгу небольшим изменением в двух частях Flux: в хранилище (store) и в действиях (actions), переключившись на неизменяемую (immutable) структуру данных для записей о вине. Когда речь заходит о React-приложениях, неизменяемость встречается довольно часто, даже если она не имеет ничего общего с библиотекой React.

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

В JavaScript, чтобы использовать идею неизменяемости, можно воспользоваться npm-пакетом immutable:

$ npm i --save-dev immutable

Нужно также дополнить содержимое файла .flowconfig:

# ....

 

[include]

# ...

node_modules/immutable

 

# ...

162156.png

Полную документацию по библиотеке можно найти в Интернете.

Поскольку вся обработка данных теперь производится в модулях хранилища и действий, то обновлять, по сути, придется только эти два модуля.

Хранилище данных  при использовании  библиотеки immutable

Библиотека immutable среди прочих предлагает такие структуры данных, как список — List, стек — Stack и отображение — Map. Выберем List, поскольку он ближе всех к массиву, который прежде использовался приложением:

/* @flow */

 

import {EventEmitter} from 'fbemitter';

import {List} from 'immutable';

 

let data: List<Object>;

let schema;

const emitter = new EventEmitter();

Обратите внимание на новый тип данных — неизменяемый List.

Новый список создается с помощью выражения let list = List() и передачи исходных значений. Посмотрим, как теперь хранилище инициализирует список:

const CRUDStore = {

 

  init(initialSchema: Array<Object>) {

    schema = initialSchema;

    const storage = 'localStorage' in window

      ? localStorage.getItem('data')

      : null;

    if (!storage) {

      let initialRecord = {};

      schema.forEach(item => initialRecord[item.id] =

        item.sample);

      data = List([initialRecord]);

    } else {

      data = List(JSON.parse(storage));

    }

  },

 

  /* .. */

};

Как видите, список инициализируется с помощью массива. Далее для работы с данными используется относящийся к спискам API-интерфейс. После создания список становится неизменяемым, то есть он не может быть изменен. (Но, как вы вскоре увидите, все манипуляции происходят в модуле CRUDActions.)

Кроме инициализации и сигнатуры типа, изменения в хранилище незначительны — все, чем оно занимается, сводится к установке и получению данных.

Следует внести одно небольшое изменение в getCount(), поскольку неизменяемый список не содержит свойства длины length:

// До

getCount(): number {

  return data.length;

},

 

// После

getCount(): number {

  return data.count(); // также работает 'data.size'

},

И наконец, нужно выполнить обновление метода getRecord(), необходимость этого обусловлена тем, что библиотека immutable не предлагает обращения по индексам (подобно встроенным массивам):

// До

getRecord(recordId: number): ?Object {

  return recordId in data ? data[recordId] : null;

},

 

// После

getRecord(recordId: number): ?Object {

  return data.get(recordId);

},

Работа с данными при использовании библиотеки immutable

Вспомним, как в JavaScript действуют методы работы со строками:

let hi = 'Hello';

let ho = hi.toLowerCase();

hi; // "Hello"

ho; // "hello"

Строка, присвоенная переменной hi, не изменяется. Вместо нее создается новая строка.

То же самое происходит и с неизменяемым списком:

let list = List([1, 2]);

let newlist = list.push(3, 4);

list.size; // 2

newlist.size; // 4

list.toArray(); // Array [ 1, 2 ]

newlist.toArray() // Array [ 1, 2, 3, 4 ]

163384.png

Обратили внимание на метод push()? Неизменяемые списки ведут себя во многом подобно массивам, поэтому для работы с ними доступны такие методы, как map(), forEach() и т.д. Отчасти именно поэтому компоненты пользовательского интерфейса, по сути, не нуждаются в изменениях. (Если говорить начистоту, то одно изменение, касающееся синтаксиса квадратных скобок для доступа к массиву, все же понадобилось.) Причина еще и в том, что, как уже упоминалось, теперь данные обрабатываются главным образом в модулях хранилища (store) и действий (actions).

Как же изменение структуры данных влияет на модуль действий (actions)? На самом деле весьма незначительно. Поскольку неизменяемый список предлагает методы sort() и filter(), по части сортировки и поиска ничего менять не нужно. Изменения касаются только методов create(), delete() и двух методов update*().

Рассмотрим метод delete():

/* @flow */

 

import CRUDStore from './CRUDStore';

import {List} from 'immutable';

 

const CRUDActions = {

 

  /* ... */

 

  delete(recordId: number) {

    // До:

    // let data = CRUDStore.getData();

    // data.splice(recordId, 1);

    // CRUDStore.setData(data);

 

    // После:

    let data: List<Object> = CRUDStore.getData();

    CRUDStore.setData(data.remove(recordId));

  },

 

  * ... */

};

 

export default CRUDActions;

Возможно, JavaScript-метод splice() имеет слегка странное название, но он возвращает извлеченную часть массива, модифицируя при этом исходный массив. Все это создает небольшую путаницу при его использовании в одной строке кода. А вот неизменяемый список позволяет все уместить в одной строке. Если бы не славная сигнатура типа, то все свелось бы к простой однострочной форме:

delete(recordId: number) {

  CRUDStore.setData(CRUDStore.getData().remove(recordId));

},

В мире библиотеки immutable имеющий однозначное толкование своего названия метод remove() не оказывает на исходный список никакого влияния, оставляя его в неизмененном виде. Метод remove() дает вам новый список с одной удаленной записью. Затем новый список назначается новыми данными для их сохранения в хранилище.

На него в этом отношении похожи и другие методы для работы с данными (их проще использовать, чем методы для работы с массивами):

/* ... */

create(newRecord: Object) {

// unshift() – как и для работы с массивами

  CRUDStore.setData(CRUDStore.getData().unshift(newRecord));

},

 

updateRecord(recordId: number, newRecord: Object) {

// set(), так как нет []

  CRUDStore.setData(CRUDStore.getData().set(recordId, newRecord));

},

 

updateField(recordId: number, key: string,

  value: string|number) {

  let record = CRUDStore.getData().get(recordId);

  record[key] = value;

  CRUDStore.setData(CRUDStore.getData().set(recordId, record));

},

/* ... */

Вот и все! Теперь у вас есть приложение, которое использует:

React-компоненты для определения пользовательского интерфейса;

JSX для создания компонентов;

Flux для организации потока данных;

неизменяемые (благодаря использованию библиотеки immu­table) данные;

Babel, чтобы воспользоваться самыми последними возможностями ECMAScript;

Flow для проверки соответствия типов и выявления синтаксических ошибок;

ESLint для дополнительной проверки на наличие ошибок и на соответствие соглашениям;

Jest для проведения блочного тестирования.

162117.png

Как и всегда, вы можете ознакомиться с полноценной рабочей версией № 3 приложения Whinepad (его «immutable-редакцией») в хранилище кода книги. А поработать с готовым приложением можно по адресу .

Назад: 7. Проверка качества кода, соответствия типов, тестирование, повтор
На главную: Предисловие

DalExcax
Весьма полезная штука ---- продажа оборудования окон пвх Здесь не может быть ошибки? ---- магазин большой одежды для женщин Я думаю, что Вы ошибаетесь. Пишите мне в PM, пообщаемся. ---- купить землю в сочи под строительство Теперь всё понятно, большое спасибо за информацию. ---- фонарь bailong bl Это просто замечательный ответ ---- отдых на море краснодарский край Присоединяюсь. Всё выше сказанное правда. Можем пообщаться на эту тему. ---- купить дом в сочи Я извиняюсь, но, по-моему, Вы не правы. Пишите мне в PM, пообщаемся. ---- как правильно кидать снюс Конечно. И я с этим столкнулся. Можем пообщаться на эту тему. ---- пицца краснодар меню Офигенно! Спасибо!!! ---- где купить аккаунт фейсбук для рекламы По моему мнению Вы не правы. ---- колумб играть бесплатно без регистрации
AnthonyTap
Это условность, ни больше, ни меньше minecraft who killed noob
stilnyeokna
Высококачественные пластиковые окошки плюс двери ото прямого изготовителя дают возможность заказчику сделать вполне индивидуальный приобретение в надобном наличие, потому наш магазин хотим клиентам сделать самые качественный сервис по производству и доставке окошек к тому же дверных проемов от иркутского изготовителя. На сайте указанного компании изготовление окон Вы можете посмотреть изготавливаемый ассортимент такой уникальной дверей затем сделать оснащение для индивидуальное помещение, коттедж либо большого жилого жилья. Наша компания указана единственным изготовщиком, в которого покупатель напрямую сможете купить пластиковые или алюминиевые окошки или двери, остекление лоджии, фасадные ансамбли у помещение, двойные входные концепции также иные виды застекления для личного помещения. Не включая низких расценок без наценок плюс диллеров компания предоставляем новым заказчикам гибкую систему уценок до сорока пяти процентов, потому по нашем сайте пользователи имеют возможность заказать окна и двери вовсе не только в Иркутске, но и на ближнюю район страны.
ecorfru
Разработка проектов касательно экологической консультации также подобная услуги, сведенная на полевыми-экологическими анализом плюс разработкой плана всяческих направлений –такая обязательная процедура, что исполняется только по закону и предоставляет выполнение норм исходя с охране внешней сферы. Компания Сиблидер выполняет полноценный величину действий именно в теме окружающей среды и предоставляет именно Вам лучшие условия сотрудничества касательно исполнение системного аспекта разработки всех видов документов и аутсорсинга на счет экологии нашими профессионалами. На источнике указанной фирмы организация государственной экологической экспертизы Вы имеют возможность пролистать всяческие подтверждающие удостоверения также разрешение, еще и заказать рекомендации от наших менеджеров либо услуги, какие фирма предоставляет. Наша компания максимально качественно просмотрим ваш заказ затем указываем наилучшие пути резолюции любой случая в небольшой промежуток времени, также еще специалисты совершаем хорошее инжиниринг юрилического лица от самого старта также к завершению договора. Заполняйте номер телефону на страничке и делайте работу в руки специалистами в области экологии каковы клиентам содействуют.
LikefilmsNet
Киносайт – является место, собственно где кинозритель найдет индивидуально такое-же по своей вкусу, определенно не найдете лучшего отвлечения от вашего рутины, чем включение захватывающего сериала обожаемого почерка, тот что зритель выбираете по личное предпочтение. Сайт качественных кинотеатра Зверополис 2 (2022) смотреть мультфильм онлайн бесплатно вмещает огромную число известных кинолент последних выпусков плюс анонсы данного 2021 года, какие постоянные посетители смогут просмотреть полностью в открытом доступе плюс без личного кабинета. Приспособленная навигация по сайту фильмов, мультфильмов либо сериалов легко обозначит кино у хорошем разрешении и звук, также Вы постоянно можете разделить популярное фильм из личными знакомыми с помощью нажатия кнопки социальной сети также написать свой отзыв, есле ж предполагаете разделить первые впечатлениями из другими зрителями. Здесь на презентованном сайте разных фильмов зритель сумеет подобрать фильмы именно по стилю, годам либо рейтингу, заходите и всегда пересматривайте лучшим кинокартиной с большим восторгом на нашему Like Films .
hd1080pred
День кинофильмов сейчас есть целостная порция вашего досуга по вечернее время и в течении дня, на свободных днях и регулярно, в момент изоляции или же небольшим сообщества потому вполне ценно найти сайт высококачественных фильмов, что постоянно рядом с добавленных. На нашем веб-сайте сборника кино Смотреть фильм Гарри Поттер и Тайная комната (2002) онлайн бесплатно и в хорошем качестве, ценители высококачественных показа смогут найти пред вышедшее показ, серийное кино или мультики, или определить видеофильм по жанру. В случае если посетитель всемирный обожатель фильмов, тогда благодаря веб-сайте фильмов есть возможность создать личный аккаунт, чтоб сохранять примечания, обозначить фильм, тот что надо посмотреть. На нашей визитной шапке всегда можно отследить популярные сюжеты, каковы ожидают на больших экранах к тому же увидеть видеоролик, а ожидаемое фильмы мы предоставляем под наших читателей только с хорошем hd разрешении, потому уверенно можете входить на ресурс потом кликнуть «Старт» указанный кино.
KinogoBlue
Зачастую мучаетесь касательно предмету, что стоит запустить интересное на вечерний досуг? На нашем онлайн кинотеатре бесплатного плюс новых кино Киного Я иду искать (2019) смотреть онлайн бесплатно пользователи можете мгновенно обнаружить хороший вариант кинофильма распространенного стиля с подмогой функционала навигации, отбора или же окна ввода. Сайт все это определил за посетителей также разработали просмотр нового кино более проще, именно на начальной стороне Вы сможет обозреть вновь свежую кино, известные многосерийное кино также предельно высокие показы, ну а во время когда надумаете увидеть трейлеры мировых фильмов этого времени, тогда кликайте в шапку «В скором времени в кино» и включайте известные спешные фильмы в прокате. Короткое воссоздание сюжета, сформированный показатель со стороны пользователей и подходящие примечания подсказывают польователю подобрать кино, какое понравится вовсе не лишь посетителю, однако и всем близким. Кликайте и ищите новую фильмы непосредственно сегодня!