Как обрабатывать отношения "один ко многим" в магазинах Flux

Я только начинаю использовать flux (с сокращением на данный момент), и мне интересно, как должны обрабатываться отношения.
Например, мы можем использовать Trello, у которого есть доски с колонками, содержащими карты.

Один из подходов состоит в том, чтобы иметь один магазин/редуктор для плат и иметь все данные в нем, но это означает некоторые очень жирные магазины, поскольку они также должны будут содержать все действия для столбцов и карточек.

Другой подход, который я видел, - это разделение вложенных ресурсов на, например, BoardStore, ColumnStore и CardStore и использование их идентификаторов в качестве ссылки.

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

Короче говоря:

  • Firing addCard
  • addCard делает запрос, тем временем вы возвращаете действие типа ADD_CARD_TEMP
  • вы получаете запрос и возвращаете действие типа ADD_CARD, где хранилище/редуктор изменяет идентификатор.

Есть ли рекомендуемый способ справиться с этим делом? Вложенный магазин/редукторы выглядят немного глупо для меня, но в противном случае вы оказываетесь в очень сложных магазинах, поэтому он действительно выглядит как компромисс.

Ответ 1

Да, использование идентификаторов в нескольких магазинах, похожих на реляционную базу данных, - это способ сделать это правильно.

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

Карты в CardStore могут выглядеть так:

_cards: {
  'CARD_1': {
    id: 'CARD_1',
    columnID: 'COLUMN_3',
    title: 'Go to sleep',
    text: 'Be healthy and go to sleep on time.',
  },
  'CARD_2': {
    id: 'CARD_2',
    columnID: 'COLUMN_3',
    title: 'Eat green vegetables',
    text: 'They taste better with onions.',
  },
}

Обратите внимание, что я могу ссылаться на карту по id, и я также могу получить идентификатор внутри объекта. Это позволяет мне иметь такие методы, как getCard(id), а также иметь возможность извлекать идентификатор определенной карты в пределах слоя представления. Таким образом, у меня может быть метод deleteCard(id), который вызывается в ответ на действие, потому что я знаю идентификатор в представлении.

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


Относительно механики оптимистичных обновлений и того, как это влияет на использование идентификаторов:

Вы можете использовать идентификатор на стороне клиента, который устанавливается в пределах одного и того же закрытия, который будет обрабатывать ответ XHR, и очистить идентификатор клиентской стороны, когда ответ вернется как успешный, или вместо этого откатиться от ошибки. Закрытие позволяет удерживать идентификатор клиентской стороны до тех пор, пока ответ не вернется.

Многие люди создадут модуль WebAPIUtils, который будет содержать все методы, связанные с закрытием, сохраняя идентификатор на стороне клиента и запрос/ответ. Создатель действия (или хранилище) может вызвать этот модуль WebAPIUtils для инициирования запроса.

Итак, у вас есть три действия:

  • инициировать запрос
  • Успех дескриптора
  • Ответ на дескриптор

В ответ на действие, инициирующее запрос, ваш магазин получает идентификатор клиентской стороны и создает запись.

В ответ на успех/ошибку ваш магазин снова получает идентификатор на стороне клиента и либо изменяет запись как подтвержденную запись с реальным идентификатором, либо вместо этого откатывает запись. Вы также хотели бы создать хороший UX вокруг этой ошибки, например, повторить попытку пользователя.

Пример кода:

// Within MyAppActions
cardAdded: function(columnID, title, text) {
  var clientID = this.createUUID();
  MyDispatcher.dispatch({
    type: MyAppActions.types.CARD_ADDED,
    id: clientID,
    columnID: columnID,
    title: title,
    text: text,
  });
  WebAPIUtils.getRequestFunction(clientID, "http://example.com", {
    columnID: columnID,
    title: title,
    text: text,
  })(); 
},

// Within WebAPIUtils
getRequestFunction: function(clientID, uri, data) {
  var xhrOptions = {
    uri: uri,
    data: data,
    success: function(response) {
      MyAppActions.requestSucceeded(clientID, response);
    },
    error: function(error) {
      MyAppActions.requestErrored(clientID, error);
    },
  };
  return function() {
    post(xhrOptions);
  };
},

// Within CardStore
switch (action.type) {

  case MyAppActions.types.CARD_ADDED:
    this._cards[action.id] = {
      id: action.id,
      title: action.title,
      text: action.text,
      columnID: action.columnID,
    });
    this._emitChange();
    break;

  case MyAppActions.types.REQUEST_SUCCEEDED:
    var tempCard = this._cards[action.clientID];
    this._cards[action.id] = {
      id: action.id,
      columnID: tempCard.columnID,
      title: tempCard.title,
      text: tempCard.text,
    });
    delete this._cards[action.clientID];
    break;

  case MyAppActions.types.REQUEST_ERRORED:
    // ...
}

Пожалуйста, не слишком увлекайтесь деталями имен и спецификой этой реализации (возможно, существуют опечатки или другие ошибки). Это просто пример кода для объяснения шаблона.