Skip to content

Latest commit

 

History

History
355 lines (313 loc) · 15.5 KB

12. Как понять Редакс без Редакса?.md

File metadata and controls

355 lines (313 loc) · 15.5 KB

Как понять Редакс без Редакса?

Редакс умещается в 100 строчек и его можно воссоздать на обычном setState. Как?

Что такое Редакс? Это библиотека для работы с состоянием, стейт-менеджер. В чём её отличие от setState? В немного другом АПИ и библиотеке react-redux, благодаря которой можно цепляться к стейту в любом компоненте.

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

К сожалению, часто люди возводят это в абсолют и все данные засовывают в стейт Редакса. Получается сложно и неудобно, появляются библиотеки типа redux-saga чтобы справиться с этим добром, развиваются мифы о сложности Редакса. Даня Абрамов, автор, настолько в ахуе, что даже написал статью You Might Not Need Redux.

Главное правило работы с Редаксом: глобальный стейт нужен только когда нужно расшарить данные между компонентами, у которых родитель находится слишком далеко.

Давайте рассмотрим на сайте jqestate.ru.

Для начала зайдём в загородную недвижимость, загрузим список объектов через АПИ и сохраним его в стейт Редакса.

Почему мы сохраняем здесь данные именно в стейт Редакса? Потому что мы их можем переиспользовать: скорее всего, с этой страницы человек перейдёт внутрь объекта (дома либо участка).

Да, мы внутри той страницы всё равно сделаем запрос (вдруг данные изменились), но мы уже можем показать то, что есть:

На этой же странице внизу есть блок «Похожие объекты», где такие же карточки. Если данные уже есть, то почему бы их не использовать?


Если бы мы не использовали Редакс, то нам бы пришлось данные кидать аж в корневой компонент <App /> и хранить в его стейте. Не очень удобно.


Кстати, а как эффективно хранить данные? С сервера приходит массив, что с ним потом делать?

const response = {
  items: [
    {
      id: 1107,
      name: "1-ое Успенское шоссе",
      aliases: [],
      propertyCategories: ["country"],
      location: {
        countryId: 1,
        regionId: 1003,
        districtId: 1012,
        routeId: 1178,
        countryName: "Россия",
        regionName: "Московская область",
        districtName: "Одинцовский",
        routeName: "Рублёво-Успенское"
      },
      createdAt: "2016-02-15T19:07:08.916377+03:00",
      updatedAt: "2016-08-23T12:22:43.472+03:00"
    },
    {
      id: 1137,
      name: "2-ое Успенское шоссе",
      aliases: [],
      propertyCategories: ["country"],
      location: {
        countryId: 1,
        regionId: 1003,
        districtId: 1012,
        routeId: 1178,
        countryName: "Россия",
        regionName: "Московская область",
        districtName: "Одинцовский",
        routeName: "Рублёво-Успенское"
      },
      createdAt: "2016-02-15T19:07:08.916377+03:00",
      updatedAt: "2016-08-23T12:22:26.629+03:00"
    },
    {
      id: 1973,
      name: "Аксаково",
      aliases: [],
      propertyCategories: ["country"],
      location: {
        countryId: 1,
        regionId: 1003,
        districtId: 1720,
        routeId: 1185,
        countryName: "Россия",
        regionName: "Московская область",
        districtName: "Мытищинский",
        routeName: "Дмитровское"
      },
      createdAt: "2016-06-06T18:02:20.503+03:00",
      updatedAt: "2016-06-06T18:02:20.503+03:00"
    },
    {
      id: 1174,
      name: "Аксиньино",
      kindName: "нас. пункт",
      aliases: [],
      propertyCategories: ["country"],
      location: {
        countryId: 1,
        regionId: 1003,
        districtId: 1012,
        routeId: 1178,
        countryName: "Россия",
        regionName: "Московская область",
        districtName: "Одинцовский",
        routeName: "Рублёво-Успенское"
      },
      createdAt: "2016-02-15T19:07:08.916377+03:00",
      updatedAt: "2016-02-15T19:07:08.916377+03:00"
    },
    {
      id: 1100,
      name: "Алабино",
      kindName: "нас. пункт",
      aliases: [],
      propertyCategories: [],
      location: {
        countryId: 1,
        regionId: 1003,
        districtId: 1721,
        routeId: 1177,
        countryName: "Россия",
        regionName: "Московская область",
        districtName: "Наро-Фоминский",
        routeName: "Киевское"
      },
      createdAt: "2016-02-15T19:07:08.916377+03:00",
      updatedAt: "2016-02-15T19:07:08.916377+03:00"
    },
    {
      id: 1112,
      name: "Александровка",
      kindName: "нас. пункт",
      aliases: [],
      propertyCategories: ["country"],
      location: {
        countryId: 1,
        regionId: 1003,
        districtId: 1009,
        routeId: 1192,
        countryName: "Россия",
        regionName: "Московская область",
        districtName: "Красногорский",
        routeName: "Ильинское"
      },
      createdAt: "2016-02-15T19:07:08.916377+03:00",
      updatedAt: "2016-02-15T19:07:08.916377+03:00"
    }
  ],
  pagination: {
    total: 258,
    limit: 32,
    offset: 0
  }
};

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

Обычно для этого пишут маппер (mapper) — функцию, которая преобразовывает из одного формата в другой, более удобный. Какой маппер могли бы мы написать?

Я предлагаю перевести массив в объект с ключами-айдишниками и завести поле list, где будет перечисление этих айдишников. Для этого мы возьмём метод .reduce у массива.

Редьюс принимает два аргумента: функцию-коллбек и начальное значение.

В функцию-коллбек приходят два аргумента: предыдущее значение и текущее, а возвращать она должна конечное значение. Она работает как аккумулятор: к накопленному прошлому добавляет текущее.

function mapItemsToObjects(items) {
  const reducedItems = items.reduce(function(prevValue, currentValue) {
    // мы будем использовать два нововведения ES2015
    // спред-оператор https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax
    // и computed property names https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Object_initializer#Computed_property_names
    return {
      ...prevValue,
      [currentValue.id]: currentValue
    };
  }, {});

  return {
    ...reducedItems, // скопируем содержимое reducedItems
    list: items.map(function(item) {
      return item.id; // пройдёмся по массиву и вернём только айдишники
    })
  };
}

const formattedProperties = mapItemsToObjects(response.items);

Что мы получили?

formattedProperties = {
  "1100": {
    id: 1100,
    name: "Алабино",
    kindName: "нас. пункт",
    aliases: [],
    propertyCategories: [],
    location: {
      countryId: 1,
      regionId: 1003,
      districtId: 1721,
      routeId: 1177,
      countryName: "Россия",
      regionName: "Московская область",
      districtName: "Наро-Фоминский",
      routeName: "Киевское"
    },
    createdAt: "2016-02-15T19:07:08.916377+03:00",
    updatedAt: "2016-02-15T19:07:08.916377+03:00"
  },
  "1107": {
    id: 1107,
    name: "1-ое Успенское шоссе",
    aliases: [],
    propertyCategories: ["country"],
    location: {
      countryId: 1,
      regionId: 1003,
      districtId: 1012,
      routeId: 1178,
      countryName: "Россия",
      regionName: "Московская область",
      districtName: "Одинцовский",
      routeName: "Рублёво-Успенское"
    },
    createdAt: "2016-02-15T19:07:08.916377+03:00",
    updatedAt: "2016-08-23T12:22:43.472+03:00"
  },
  "1112": {
    id: 1112,
    name: "Александровка",
    kindName: "нас. пункт",
    aliases: [],
    propertyCategories: ["country"],
    location: {
      countryId: 1,
      regionId: 1003,
      districtId: 1009,
      routeId: 1192,
      countryName: "Россия",
      regionName: "Московская область",
      districtName: "Красногорский",
      routeName: "Ильинское"
    },
    createdAt: "2016-02-15T19:07:08.916377+03:00",
    updatedAt: "2016-02-15T19:07:08.916377+03:00"
  },
  "1137": {
    id: 1137,
    name: "2-ое Успенское шоссе",
    aliases: [],
    propertyCategories: ["country"],
    location: {
      countryId: 1,
      regionId: 1003,
      districtId: 1012,
      routeId: 1178,
      countryName: "Россия",
      regionName: "Московская область",
      districtName: "Одинцовский",
      routeName: "Рублёво-Успенское"
    },
    createdAt: "2016-02-15T19:07:08.916377+03:00",
    updatedAt: "2016-08-23T12:22:26.629+03:00"
  },
  "1174": {
    id: 1174,
    name: "Аксиньино",
    kindName: "нас. пункт",
    aliases: [],
    propertyCategories: ["country"],
    location: {
      countryId: 1,
      regionId: 1003,
      districtId: 1012,
      routeId: 1178,
      countryName: "Россия",
      regionName: "Московская область",
      districtName: "Одинцовский",
      routeName: "Рублёво-Успенское"
    },
    createdAt: "2016-02-15T19:07:08.916377+03:00",
    updatedAt: "2016-02-15T19:07:08.916377+03:00"
  },
  "1973": {
    id: 1973,
    name: "Аксаково",
    aliases: [],
    propertyCategories: ["country"],
    location: {
      countryId: 1,
      regionId: 1003,
      districtId: 1720,
      routeId: 1185,
      countryName: "Россия",
      regionName: "Московская область",
      districtName: "Мытищинский",
      routeName: "Дмитровское"
    },
    createdAt: "2016-06-06T18:02:20.503+03:00",
    updatedAt: "2016-06-06T18:02:20.503+03:00"
  },
  list: [1107, 1137, 1973, 1174, 1100, 1112]
};

Для чего мы это сделали? Когда нам нужно обратиться к конкретному айдишнику — мы пишем formattedProperties[1100], но при этом мы можем итерироваться (проходить один за одним) по айдишникам, которые у нас лежат в formattedProperties.list.

Поэтому когда нам нужно будет вывести список с данными, мы напишем очень простой код:

function PropertiesList() {
  return (
    <ul>
      {formattedProperties.list.map(function(id) {
        const data = formattedProperties[id];

        return (
          <li>
            <a href={`/properties/${data.id}`}>
              {data.name} на шоссе {data.location.routeName}
            </a>
          </li>
        );
      })}
    </ul>
  );
}

Без этого нам бы пришлось городить response.items.find(function(item) { return item.id === 1001 }). Не очень удобно.

Готовим АПИ Редакса в домашних условиях

Окей, со смыслом Редакса и даже с форматтерами мы разобрались, теперь давайте воссоздадим АПИ Редакса на setState.

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

Вы знали, что в setState() можно передать не только объект, но и функцию, которая должна вернуть новый стейт? Это называется функциональным сетСтейтом. У функции есть аргумент prevState — предыдущий стейт, он-то там и понадобится.

Что такое экшен Редакса? Это обычный объект, в котором есть поле type. По этому полю редьюсер понимает, что нужно сделать со стейтом. Экшен вызывается через диспатч.

Пора воссоздавать!

iframe: https://codesandbox.io/embed/48rnjr52lw?module=%2Fsrc%2FCounter.js&view=editor

Итог

Сегодня мы разобрались с философией Редакса: в глобальном стейте храним только то, что нельзя переиспользовать через родителей; заодно мы познакомились с концепцией мапперов/трансформеров/форматтеров: функций, которые переводят данные из одного вида в другой.

После этого мы воссоздали АПИ Редакса с его редьюсерами-экшенами-диспатчем на самом обычном setState() и познакомились с функциональным setState()-ом.