И в чем же проблема?


Я начал изучать React и Redux не так давно, но он уже успел изрядно потрепать мне нервы. Буквально над каждым действием приходится задумываться — почти никакие изменения в коде невозможны без того, чтоб что-то оторвать. Чтоб просто получить список постов по API и вывести их, надо, пожалуй, написать не меньше сотни строк кода — создать корневой контейнер, создать store, добавить action для запроса к API, для успешного результата запроса, для неудачного результата запроса, создать action-creators, сматчить action-creators и props, сматчить dispatch и props, написать reducer на каждый action… Ух, продолжать не хочется. И все это мы должны делать заново для каждого веб-приложения — крайне нерациональная трата сил программиста.


Да, можно сказать новичку: "Смотри, тут десяток пакетов, которые могут сделать каждое действие из этого списка вместо тебя. Выбирай и пользуйся!" Но проблема в том, что надо разобраться в настройке и воспользоваться десятком пакетов, позаботившись о том, чтоб они совпадали с версией, которая описана в документации и не вступали друг с другом в конфликты… Слишком сложно. Хочется чего-то проще, такого же простого, как в мире Django, из которого я пришел. Какой-то один пакет, после установки которого в store сами по волшебству складываются все нужные данные — бери и пользуйся.


Ну, я и решил — если такого решения нет, напишу-ка я его сам.


Постановка задачи


Убирая всю лирику из первого абзаца, получаю задачу — нам нужно создать инструмент, который будет:


  1. Делать асинхронный GET-запрос к REST API.
  2. Анализировать полученные данные и данные, лежащие в store, и, если там не хватает связанных по foreign key данных, делать еще запросы.
  3. Складывать полученные данные в store и следить за актуальностью хранящихся данных.

По описанию выходит, что состоять пакет будет из action creator'а, middleware и reducer'а.


Инструменты


К счастью, как было сказано в первом абзаце, очень многие вещи на JS уже давно написаны, и писать их заново не придется. Например, ходить в API мы будем с помощью redux-api-middleware, следить за неизменяемостью данных будем с помощью react-addons-update, а нормализовать данные (куда же без этого?) будем с помощью normalizr.


Конфигурация


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


1. Опишем схему данных со связанными сущностями на примере постов и юзеров:


    const schema = {
        users: {},
        posts: {
            author: "users"
        }
    };

Что-то напоминает, правда? Похоже на schema.Entity из normalizr. да, можно было использовать сразу классы из normalizr, но я считаю, что это пойдет во вред удобству конфига. В normalizr ключ должен ссылаться не просто на строку, как в нашем конфиге, а на объект entity, и конфиг превратился бы в это:


    import {schema} from 'normalizr';
    const user = new schema.Entity("users", {});
    const post = new schema.Entity("posts", {author: user});
    const normalizrSchema = {
        users: user,
        posts: post,
    }

И это намного менее красиво и удобно, чем первый вариант.


2. Точки входа и actions для API.


Тут мы будем следовать обратной логике — если есть удобный способ конфигурации, написанный ком-то до нас, зачем его менять? Сформируем конфиг с параметрами, которые передаются в action в redux-api-middleware, и получится довольно удобно:


    const api = {
        users: {
            endpoint: "mysite.com/api/users/",
            types: ['USERS_GET', 'USERS_SUCCESS', 'USERS_FAILURE'],
        },
        posts: {
            endpoint: "mysite.com/api/posts/",
            types: ['POSTS_GET', 'POSTS_SUCCESS', 'POSTS_FAILURE'],
        }
    };

Конечно, все типы action можно объявить отдельными переменными, а не строками — тут это сделано исключительно для простоты. Реализуем мы только GET-запросы, поэтому нет нужды в поле method.


3. "Время жизни" данных в store.


Конечно, рано или поздно данные на клиенте теряют актуальность — нам нельзя слепо полагаться на данные, которые когда-то давно к нам пришли с сервера. Поэтому надо предусмотреть механизм инвалидации старых данных и записать "время жизни" каждого типа данных в конфиг.


    const lifetime = {
        users: 20000,
        posts: 100000
    };

Соберем все части конфига воедино:


    const config = {schema, api, lifetime};

Таким образом, все довольно просто — юзеры "живут" в store 20 секунд, а посты — 100 секунд. Как только время жизни выйдет, мы должны будем идти за данными, даже если они уже хранятся в store, значит, нужно будет запоминать время прихода данных. И это нас подводит к следующему пункту — планированию store.


Планирование store


В этом пункте все довольно просто — нам нужно хранить данные и время их прихода. Заведем два ключа в store — entities и timestamp. Для уже знакомых с normalizr сразу становится понятно — в entities мы будем хранить наши сущности, и выглядеть он будет как-то так:


     const entities = {
         posts: {1: {id: 1, content: "content", author: 1}, 2: {id: 2, content: "not content", author: 2}},
         users: {1: {id: 1, username: "one"}, 2: {id: 2, username: "two"}}
     };

То есть, это словарь с ключами-сущностями, каждая из которых, в свою очередь, словарь с ключами-id моделей.


timestamp же будет выглядеть очень похоже, но по id мы будем получать не данные, а момент доставки данных клиенту — Date.now().


     const timestamp = {
         posts: {1: 1496618924981, 2: 1496618924981},
         users: {1: 1496618924983, 2: 1496618924983}
     };

На этом, в общем-то, пока все. В следующей части будет описан процесс разработки самих компонентов.

Поделиться с друзьями
-->

Комментарии (14)


  1. Veikedo
    07.06.2017 14:06

    Анализировать полученные данные и данные, лежащие в store, и, если там не хватает связанных по foreign key данных, делать еще запросы.

    Что-то этого в статье не видно.
    Явно, должно быть ещё middleware.


    Кстати, схему делал в проекте с помощью https://github.com/mchlbrnd/normalizr-decorators с автогенерацией из моделей бекенда


    1. geoolekom
      08.06.2017 00:37

      Про middleware я написал в разделе «постановка задачи». Конечно, в статье его ещё нет — ведь компоненты описывать я буду только в следующей части. Там появится и middleware, и reducer, и action creator.


  1. raveclassic
    07.06.2017 16:57
    +2

    А все потому, что не нужно пытаться пихать в стор все подряд. Redux — это не про «хранение всего состояния приложения», хотя может показаться, что это так.

    Нужен вам local-запрос, ну так сделайте его в контейнере, ничего криминального в этом нет. А если вам нужно в нескольких частях приложения отреагировать на его результат или ошибку — используйте redux.


    1. Veikedo
      07.06.2017 17:44

      А по-моему, это как раз про то.
      Имея middleware и единую шину, мы можем контролировать и реагировать на любое действие централизованно, а не позволять произвол на местах (в контейнерах).
      Взять ту же обработку ошибок/логгирование/телеметрию.


      +без redux надо shouldComponentUpdate писать самому


      1. raveclassic
        07.06.2017 17:53

        Никто не спорит. Я лишь говорю, что делаться это должно с умом, без фанатизма и только там, где нужны эти реакции. Однако же вам придется заново компоновать такие запросы, так как они будут скорей всего нормализованы и уже собранное компонентами/контейнерами дерево композиции потеряется. И тут тоже ничего плохого нет, просто в 99% случаев этот момент — причина непонимания и недовольства.


  1. Tim152
    08.06.2017 00:37

    Конечно, рано или поздно данные на клиенте теряют актуальность — нам нельзя слепо полагаться на данные, которые когда-то давно к нам пришли с сервера. Поэтому надо предусмотреть механизм инвалидации старых данных и записать «время жизни» каждого типа данных в конфиг.

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


    1. geoolekom
      08.06.2017 00:38

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


      1. raveclassic
        08.06.2017 02:00

        Можно же поднять посредника в лице чего-нибудь в духе pouchdb, и общаться как на клиенте, так и на бэке с этим хранилищем.


  1. jankovsky
    08.06.2017 13:47
    -1

    Солидарен с автором на счет сложности, поэтому отказался от этих всех вебпаков, но свой велосипед не хочется писать. Jquery или mootools побеждает это вот все.


  1. Druu
    08.06.2017 15:28

    А в чем преимущество предложенного решения перед стандартный workflow? Точно так же ведь надо объявить типы экшенов, создать экшен криэйторы, написать редьюсеры. Что изменилось?


    1. geoolekom
      08.06.2017 15:28

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


      1. Druu
        09.06.2017 03:44

        У вас одинаковые проекты что ли? Ну тогда да, повезло.


        1. geoolekom
          09.06.2017 04:41

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


          1. Druu
            09.06.2017 08:09

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