Redux: попытка избавиться от потребности думать во время запросов к API

в 10:57, , рубрики: api, ES6, javascript, middleware, npm, React, ReactJS, redux, Разработка веб-сайтов

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

Я начал изучать 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}
     };

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

Автор: geoolekom

Источник

Поделиться

* - обязательные к заполнению поля