И в чем же проблема?
Я начал изучать React и Redux не так давно, но он уже успел изрядно потрепать мне нервы. Буквально над каждым действием приходится задумываться — почти никакие изменения в коде невозможны без того, чтоб что-то оторвать. Чтоб просто получить список постов по API и вывести их, надо, пожалуй, написать не меньше сотни строк кода — создать корневой контейнер, создать store, добавить action для запроса к API, для успешного результата запроса, для неудачного результата запроса, создать action-creators, сматчить action-creators и props, сматчить dispatch и props, написать reducer на каждый action… Ух, продолжать не хочется. И все это мы должны делать заново для каждого веб-приложения — крайне нерациональная трата сил программиста.
Да, можно сказать новичку: "Смотри, тут десяток пакетов, которые могут сделать каждое действие из этого списка вместо тебя. Выбирай и пользуйся!" Но проблема в том, что надо разобраться в настройке и воспользоваться десятком пакетов, позаботившись о том, чтоб они совпадали с версией, которая описана в документации и не вступали друг с другом в конфликты… Слишком сложно. Хочется чего-то проще, такого же простого, как в мире Django, из которого я пришел. Какой-то один пакет, после установки которого в store сами по волшебству складываются все нужные данные — бери и пользуйся.
Ну, я и решил — если такого решения нет, напишу-ка я его сам.
Постановка задачи
Убирая всю лирику из первого абзаца, получаю задачу — нам нужно создать инструмент, который будет:
- Делать асинхронный GET-запрос к REST API.
- Анализировать полученные данные и данные, лежащие в store, и, если там не хватает связанных по foreign key данных, делать еще запросы.
- Складывать полученные данные в 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)
raveclassic
07.06.2017 16:57+2А все потому, что не нужно пытаться пихать в стор все подряд. Redux — это не про «хранение всего состояния приложения», хотя может показаться, что это так.
Нужен вам local-запрос, ну так сделайте его в контейнере, ничего криминального в этом нет. А если вам нужно в нескольких частях приложения отреагировать на его результат или ошибку — используйте redux.Veikedo
07.06.2017 17:44А по-моему, это как раз про то.
Имея middleware и единую шину, мы можем контролировать и реагировать на любое действие централизованно, а не позволять произвол на местах (в контейнерах).
Взять ту же обработку ошибок/логгирование/телеметрию.
+без redux надо shouldComponentUpdate писать самому
raveclassic
07.06.2017 17:53Никто не спорит. Я лишь говорю, что делаться это должно с умом, без фанатизма и только там, где нужны эти реакции. Однако же вам придется заново компоновать такие запросы, так как они будут скорей всего нормализованы и уже собранное компонентами/контейнерами дерево композиции потеряется. И тут тоже ничего плохого нет, просто в 99% случаев этот момент — причина непонимания и недовольства.
Tim152
08.06.2017 00:37Конечно, рано или поздно данные на клиенте теряют актуальность — нам нельзя слепо полагаться на данные, которые когда-то давно к нам пришли с сервера. Поэтому надо предусмотреть механизм инвалидации старых данных и записать «время жизни» каждого типа данных в конфиг.
— такое решение пороховая бочка и кладезь ошибок, лучше подключить вебсокеты и тогда ваше хранилище станет отражением сервераgeoolekom
08.06.2017 00:38Да, можно, но тут инвалидация данных будет зависеть от реализации бэкэнда, а от этого хочется отойти.
raveclassic
08.06.2017 02:00Можно же поднять посредника в лице чего-нибудь в духе pouchdb, и общаться как на клиенте, так и на бэке с этим хранилищем.
jankovsky
08.06.2017 13:47-1Солидарен с автором на счет сложности, поэтому отказался от этих всех вебпаков, но свой велосипед не хочется писать. Jquery или mootools побеждает это вот все.
Druu
08.06.2017 15:28А в чем преимущество предложенного решения перед стандартный workflow? Точно так же ведь надо объявить типы экшенов, создать экшен криэйторы, написать редьюсеры. Что изменилось?
geoolekom
08.06.2017 15:28Просто этот модуль мы пишем один раз и используем во всех проектах, правя только конфиги.
Druu
09.06.2017 03:44У вас одинаковые проекты что ли? Ну тогда да, повезло.
geoolekom
09.06.2017 04:41Прелесть в том, что если проект базируется на REST API, то этот пакет будет работать для всех таких проектов.
Druu
09.06.2017 08:09Ни разу не встречал такой ситуации, когда экшоны на клиенте четко соответствуют апи. Обычно отдельно слой взаимодействия с бекендом, отдельно — экшоны (они зависят от функциональности клиента, а не от апи, апи и слой взаимодействия с ним как раз могут быть заменены, а экшоны останутся теми же). Если в вашем случае это соответствие есть — тогда да, будет удобно все работать.
Veikedo
Что-то этого в статье не видно.
Явно, должно быть ещё middleware.
Кстати, схему делал в проекте с помощью https://github.com/mchlbrnd/normalizr-decorators с автогенерацией из моделей бекенда
geoolekom
Про middleware я написал в разделе «постановка задачи». Конечно, в статье его ещё нет — ведь компоненты описывать я буду только в следующей части. Там появится и middleware, и reducer, и action creator.