Состояние как массив объектов против объекта с ключом id

97

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

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

Они продолжают утверждать

Думайте о состоянии приложения как о базе данных.

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

Так что я думал об этом как

[{
    id: '1',
    name: 'View',
    open: false,
    options: ['10', '11', '12', '13'],
    selectedOption: ['10'],
    parent: null,
  },
  {
    id: '10',
    name: 'Time & Fees',
    open: false,
    options: ['20', '21', '22', '23', '24'],
    selectedOption: null,
    parent: '1',
  }]

Однако в документах предлагается формат, больше похожий на

{
   1: { 
    name: 'View',
    open: false,
    options: ['10', '11', '12', '13'],
    selectedOption: ['10'],
    parent: null,
  },
  10: {
    name: 'Time & Fees',
    open: false,
    options: ['20', '21', '22', '23', '24'],
    selectedOption: null,
    parent: '1',
  }
}

Теоретически это не имеет значения, если данные можно сериализовать (под заголовком «Состояние») .

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

При подходе объектного ключа по идентификатору (и либерального использования синтаксиса распространения) OPEN_FILTERчасть редуктора становится

switch (action.type) {
  case OPEN_FILTER: {
    return { ...state, { ...state[action.id], open: true } }
  }

Принимая во внимание, что подход с использованием массива объектов, он более подробный (и зависит от вспомогательных функций)

switch (action.type) {
   case OPEN_FILTER: {
      // relies on getFilterById helper function
      const filter = getFilterById(state, action.id);
      const index = state.indexOf(filter);
      return state
        .slice(0, index)
        .concat([{ ...filter, open: true }])
        .concat(state.slice(index + 1));
    }
    ...

Итак, у меня три вопроса:

1) Является ли простота редуктора мотивацией для перехода к подходу с ключом объекта по идентификатору? Есть ли у этой государственной формы другие преимущества?

а также

2) Похоже, что подход с использованием объектного ключа по идентификатору усложняет работу со стандартным вводом / выводом JSON для API. (Вот почему я в первую очередь выбрал массив объектов.) Итак, если вы придерживаетесь этого подхода, вы просто используете функцию для преобразования его между форматом JSON и форматом формы состояния? Это кажется неуклюжим. (Хотя, если вы отстаиваете этот подход, вы считаете, что это менее неуклюже, чем редуктор массива объектов выше?)

а также

3) Я знаю, что Дэн Абрамов разработал redux так, чтобы теоретически не зависеть от структуры данных состояния (как предполагает «По соглашению, состояние верхнего уровня - это объект или некоторая другая коллекция значений ключа, например Map, но технически это может быть любое типа " курсив мой). Но, учитывая вышеизложенное, просто «рекомендуется» сохранить его как объект с ключом по идентификатору, или есть другие непредвиденные болевые точки, с которыми я собираюсь столкнуться, используя массив объектов, которые делают его таким, что я должен просто прервать это планировать и пытаться придерживаться объекта с ключом по ID?

Nickcoxdotme
источник
2
Это интересный вопрос, и он у меня тоже был, просто чтобы дать некоторое представление, хотя я склонен нормализовать редукцию вместо массивов (просто потому, что искать проще), я обнаружил, что если вы выберете нормализованный подход, сортировка станет проблема, потому что вы не получаете ту же структуру, что и массив, поэтому вы вынуждены сортировать себя.
Роберт Сондерс
Я вижу проблему в подходе «объектный ключ по идентификатору», однако это нечасто, но мы должны учитывать этот случай при написании любого UI-приложения. Так что, если я хочу изменить порядок объекта с помощью элемента перетаскивания, указанного в упорядоченном списке? Обычно подход «объектный ключ по идентификатору» здесь не работает, и я бы наверняка выбрал массив объектного подхода, чтобы избежать таких серьезных проблем.
Могло
Как можно отсортировать объект, состоящий из объектов? Это кажется невозможным.
Дэвид Вильхубер
@DavidVielhuber Вы имеете в виду, помимо использования чего-то вроде lodash's sort_by? const sorted = _.sortBy(collection, 'attribute');
nickcoxdotme
Да. В настоящее время мы конвертируем эти объекты в массивы внутри вычисляемого свойства vue
Дэвид Вильхубер

Ответы:

47

Q1: Простота редуктора - результат того, что вам не нужно искать в массиве нужную запись. Преимуществом является отсутствие необходимости искать в массиве. Селекторы и другие средства доступа к данным могут и часто имеют доступ к этим элементам с помощью id. Необходимость поиска в массиве для каждого доступа становится проблемой производительности. Когда ваши массивы становятся больше, проблема производительности резко ухудшается. Кроме того, по мере того, как ваше приложение становится более сложным, отображая и фильтруя данные в большем количестве мест, проблема также усугубляется. Комбинация может быть вредной. При доступе к элементам по id, время доступа изменяется с O(n)на O(1), что для больших n(здесь элементов массива) имеет огромное значение.

Q2: вы можете использовать, normalizrчтобы помочь вам с преобразованием из API в магазин. Начиная с normalizr V3.1.0, вы можете использовать denormalize, чтобы пойти другим путем. Тем не менее, приложения часто являются больше потребителями, чем производителями данных, и поэтому преобразование в хранилище обычно выполняется чаще.

Q3: Проблемы, с которыми вы столкнетесь при использовании массива, связаны не столько с условиями хранения и / или несовместимостью, сколько с проблемами производительности.

DDS
источник
normalizer - это снова то, что наверняка вызовет боль, если мы изменим defs в backend. Так что это должно обновляться каждый раз
Кунал Навхате
12

Думайте о состоянии приложения как о базе данных.

Это ключевая идея.

1) Наличие объектов с уникальными идентификаторами позволяет вам всегда использовать этот идентификатор при обращении к объекту, поэтому вы должны передавать минимальный объем данных между действиями и редукторами. Это более эффективно, чем использование array.find (...). Если вы используете массивный подход, вам нужно передать весь объект, и это может очень скоро стать беспорядочным, вы можете в конечном итоге воссоздать объект на разных редукторах, действиях или даже в контейнере (вы этого не хотите). Представления всегда смогут получить полный объект, даже если связанный с ними редуктор содержит только идентификатор, потому что при сопоставлении состояния вы где-то получите коллекцию (представление получает все состояние, чтобы сопоставить его со свойствами). Из-за всего того, что я сказал, действия в конечном итоге имеют минимальное количество параметров и сокращают минимальный объем информации, попробуйте,

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

3) Я использовал массивы для структур с идентификаторами, и это непредвиденные последствия, которые я понес:

  • Постоянное воссоздание объектов по всему коду
  • Передача ненужной информации редукторам и действиям
  • Как следствие, плохой, не чистый и не масштабируемый код.

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

Также:

4) Большинство коллекций с идентификаторами предназначены для использования идентификатора в качестве ссылки на весь объект, вы должны воспользоваться этим. Вызовы API получат идентификатор, а затем остальные параметры, а также ваши действия и редукторы.

Марко Скаббиоло
источник
Я сталкиваюсь с проблемой, когда у нас есть приложение с большим количеством данных (от 1000 до 10000), хранящихся по id в объекте в хранилище redux. В представлениях все они используют отсортированные массивы для отображения данных временных рядов. Это означает, что каждый раз, когда выполняется повторная визуализация, он должен принимать весь объект, преобразовывать его в массив и отсортировать его. Мне было поручено повысить производительность приложения. Является ли это вариантом использования, когда имеет смысл хранить данные в отсортированном массиве и использовать двоичный поиск для удаления и обновления вместо объекта?
Уильям Чоу
В итоге мне пришлось создать некоторые другие хэш-карты, полученные из этих данных, чтобы минимизировать время вычислений при обновлениях. Это приводит к тому, что для обновления всех различных представлений требуется собственная логика обновления. До этого все компоненты брали объект из хранилища и перестраивали структуры данных, необходимые для его представления. Единственный способ, который я могу придумать для минимизации проблем с пользовательским интерфейсом, - это использование веб-воркера для преобразования объекта в массив. Компромисс для этого - более простая логика поиска и обновления, поскольку все компоненты зависят только от одного типа данных для чтения и записи.
Уильям Чоу
8

1) Является ли простота редуктора мотивацией для перехода к подходу с ключом объекта по идентификатору? Есть ли у этой государственной формы другие преимущества?

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

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

[{
  id: 1,
  name: 'View',
  open: false,
  options: [
    {
      id: 10, 
      title: 'Option 10',
      created_by: { 
        id: 1, 
        username: 'thierry' 
      }
    },
    {
      id: 11, 
      title: 'Option 11',
      created_by: { 
        id: 2, 
        username: 'dennis'
      }
    },
    ...
  ],
  selectedOption: ['10'],
  parent: null,
},
...
]

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

Лучшим решением было бы нормализовать ответ до:

results: [1],
entities: {
  filterItems: {
    1: {
      id: 1,
      name: 'View',
      open: false,
      options: [10, 11],
      selectedOption: [10],
      parent: null
    }
  },
  options: {
    10: {
      id: 10,
      title: 'Option 10',
      created_by: 1
    },
    11: {
      id: 11,
      title: 'Option 11',
      created_by: 2
    }
  },
  optionCreators: {
    1: {
      id: 1,
      username: 'thierry',
    },
    2: {
      id: 2,
      username: 'dennis'
    }
  }
}

С такой структурой намного проще и эффективнее перечислить всех пользователей, которые создали параметры (у нас они изолированы в entity.optionCreators, поэтому нам просто нужно пройтись по этому списку).

Также довольно просто показать, например, имена пользователей тех, кто создал параметры для элемента фильтра с ID 1:

entities
  .filterItems[1].options
  .map(id => entities.options[id])
  .map(option => entities.optionCreators[option.created_by].username)

2) Похоже, что подход с использованием объектного ключа по идентификатору усложняет работу со стандартным вводом / выводом JSON для API. (Вот почему я в первую очередь выбрал массив объектов.) Итак, если вы придерживаетесь этого подхода, вы просто используете функцию для преобразования его взад и вперед между форматом JSON и форматом формы состояния? Это кажется неуклюжим. (Хотя, если вы отстаиваете такой подход, вы считаете, что это менее неуклюже, чем редуктор массива объектов выше?)

JSON-ответ можно нормализовать, например, с помощью normalizr .

3) Я знаю, что Дэн Абрамов разработал redux так, чтобы теоретически не зависеть от структуры данных состояния (как предполагает «По соглашению, состояние верхнего уровня - это объект или некоторая другая коллекция значений ключа, например Map, но технически это может быть любое типа "курсив мой). Но, учитывая вышеизложенное, просто «рекомендуется» сохранить его как объект с ключом по идентификатору, или есть другие непредвиденные болевые точки, с которыми я собираюсь столкнуться, используя массив объектов, которые делают его таким, что я должен просто прервать это планировать и пытаться придерживаться объекта с ключом по ID?

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

Tobiasandersen
источник
2
mapвозвращает undefined, как здесь , если ресурсы выбираются отдельно, что filterслишком усложняет задачу. Есть решение?
Сараванабалаги Рамачандран
1
@tobiasandersen, как вы думаете, нормально ли, чтобы сервер возвращал нормализованные данные, идеально подходящие для реакции / сокращения, чтобы клиент не выполнял преобразование через библиотеки, такие как normalizr? Другими словами, заставьте сервер нормализовать данные, а не клиент.
Мэтью