Итерировать с помощью Object.keys () и отбросить по идентификатору

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



Структура выглядела так:

const reduxStore = {
  data: {
    byId: ['a', 'b'],
    byHash: {
      a: {someKey: "someValue", id: "a"},
      b: {someKey: "someOtherValue", id: "b"}
    }
  }
}

Любое действие, которое изменяет эту структуру данных, будет обрабатываться дважды: один раз для добавления / удаления любых ключей в массиве byId и снова для добавления / удаления / обновления связанных данных, хранящихся в хэше byHash. Однако теперь, когда я использую эту структуру в течение нескольких месяцев, я обнаружил, что большую часть времени я обычно обходюсь без массива byId. Итак, моя структура:

const reduxStore = {
  data: {
    a: {someKey: "someValue", id: "a"},
    b: {someKey: "someOtherValue", id: "b"}
  }
}

Почему я роняю byId?

Плюсы:

  • Обработка действий проще, поскольку мне нужно обновить только одну структуру данных в ответ на наиболее распространенные действия.
  • Объекты хранилища Redux имеют меньшую вложенность
  • Итерация легко достигается с помощью Object.keys(data).forEach или, что более типично, в приложении React: Object.keys(data).map.
  • Длина доступна как Object.keys(data).length.

Ожидаемые минусы:

  • Никто? (Что мне не хватает?)

Edge-case Минусы:

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

Шаблон, который я использую повсюду как результат

Наиболее типичный вариант использования состоит в том, что мне часто нужно перебирать эту структуру данных (forEach) или выполнять какую-то функциональную операцию (карта / фильтр / уменьшение). Этого легко добиться:

  1. Отображение списков компонентов React
{Object.keys(this.props.data)
  .map(key => {
    // operate on the full value since `key` is just the key
    const renderData = this.props.data[key]; 
    return <div>{renderData.someValue}</div>
  })
}

2. Фильтрация на основе некоторого значения в каждом объекте.

{Object.keys(this.props.data)
  .filter(key => {
    // again, operate on the full value, not the key
    return this.props.data[key].value === condition;
  })
  .map( ....)
}

Вы можете использовать этот базовый паттерн снова и снова. Хотите отсортировать по некоторому значению в объекте? Object.keys(data).sort((a,b) => {}) Хотите, чтобы некоторые данные не отображались, если список имеет нулевую длину? Object.keys(data).length === 0 && <Component /> Все проблемы решаются одним и тем же основным фрагментом кода.

Конечно, это немного избыточно, но на самом деле это не хуже, чем:

{data.byId.map(id => {
  const renderData = data.byHash[id];
  return <div>{renderData.someValue}</div>
})}

Соображения производительности

Спасибо Марку Эриксону за отличный комментарий о некоторых последствиях для производительности при перемещении логики фильтрации в цикл рендеринга.

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

Вдобавок, если mapState возвращает одни и те же значения от вызова к вызову, тогда connect пропустит повторный рендеринг простого компонента, что обычно является улучшением производительности.

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

render() {
  return (
    <div>{Object.keys(this.props.data).map(key => {
      const val = this.props.data[key]
      return (<span>{val.text}</span>)
    })</div>
  )
}
const mapStateToProps = state => ({ data: state.myData })
// could easily become this to clean up logic:
render() {
  return (
    <div>{this.props.data.map(val => (
      <span>{val.text}</span>
    ))}</div>
  )
}
const mapStateToProps = state => ({ 
  data: Object.keys(state.myData).map(key => state.myData[key])
})

Вы даже можете воссоздать шаблон byId и byHash в своем селекторе, чтобы получить его преимущества без необходимости поддерживать параллельный массив в вашем магазине.

const mapStateToProps = state => {
  const myKeys = Object.keys(state.myData)
  return {
    byId: myKeys,
    byHash: myKeys.map(key => state.myData[key])
  }
}

Как всегда, оставьте комментарий, если вы не согласны или если увидите что-то, что я упустил.