Redux: попытка избавиться от потребности думать во время запросов к API
- четверг, 8 июня 2017 г. в 03:14:55
Я начал изучать React и Redux не так давно, но он уже успел изрядно потрепать мне нервы. Буквально над каждым действием приходится задумываться — почти никакие изменения в коде невозможны без того, чтоб что-то оторвать. Чтоб просто получить список постов по API и вывести их, надо, пожалуй, написать не меньше сотни строк кода — создать корневой контейнер, создать store, добавить action для запроса к API, для успешного результата запроса, для неудачного результата запроса, создать action-creators, сматчить action-creators и props, сматчить dispatch и props, написать reducer на каждый action… Ух, продолжать не хочется. И все это мы должны делать заново для каждого веб-приложения — крайне нерациональная трата сил программиста.
Да, можно сказать новичку: "Смотри, тут десяток пакетов, которые могут сделать каждое действие из этого списка вместо тебя. Выбирай и пользуйся!" Но проблема в том, что надо разобраться в настройке и воспользоваться десятком пакетов, позаботившись о том, чтоб они совпадали с версией, которая описана в документации и не вступали друг с другом в конфликты… Слишком сложно. Хочется чего-то проще, такого же простого, как в мире Django, из которого я пришел. Какой-то один пакет, после установки которого в 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 — 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}
};
На этом, в общем-то, пока все. В следующей части будет описан процесс разработки самих компонентов.