Редактирование данных
Ранее мы рассмотрели, как читать данные, но нам ещё нужно как-то реагировать на действия пользователя, чтобы и зменять данные.
Декоратор daemon
Давайте создадим ещё одну сущность — Заказ. Кроме того, что мы можем загрузить заказы, мы ещё можем создать новый заказ.
Для вызова методов сервиса вне саг воспользуемся декоратором daemon
class OrderService extends Service {
toString() {
return "OrderService"
}
@operation
*getOrders() {
return yield* call(fetchOrders);
}
// Создает redux экшен для этого метода.
// Это позволит нам вызывать его откуда захотим, а не только из useSaga
@daemon()
*addOrder() {
const id = getNewId();
yield* call(addOrder, {id, description: `Order ${id}`});
// обновляем данные о заказах в store
yield* call(this.getOrders);
}
}
Давайте добавим в приложение наш сервис
function App({children}) {
const di = useDI();
const userService = di.createService(UserService);
di.registerService(userService)
const orderService = di.createService(OrderService);
di.registerService(orderService)
const {operationId} = useService([userService, orderService]);
return (...);
}
Теперь напишем интерфейс, который будет предоставлять управление заказами.
Не рекомендуется вызывать методы сервиса из других саг через actions, тк такой вызов не будет отменен при отмене родительской саги.
useSaga({
onLoad: function * () {
// не правильно, на unmount компонента onLoad будет отменен, а foo - нет
actions.foo();
// правильно
yield call(service.foo)
}
})
Поведение по умолчанию декоратора daemon — метод не будет вызван, пока не отработал текущий вызов (касается только вызова через actions). Это позволяет из коробки избегать избыточных срабатываний — лишние клики по кнопкам, множественные события скролла при пагинации и т. д.
Это поведение можно поменять указав аргументы декоратора, подробнее читайте в описании.
Стратегии для операций
Можно заметить, что каждый раз, когда мы добавляем заказ, мы видим лоадер — это не очень приятный UX. К тому же мы делаем лишние запросы за списком заказов. Давайте сделаем добавление заказа на клиентской стороне.
Для этого можно написать стратегию обновления операции, которая может преобразовывать данные перед их записью в store. У стратегии очень простой контракт - она принимает на вход данные операции, и должна их вернуть в том же формате, включая тип результата операции.
// Заведем явный id для операций над списком заказов.
// Это позволит нам редактировать одни и те же данные в store разными методами.
const ORDERS_OPERATION_ID = 'orders' as OperationId<Order[]>
// опишем стратегию, которая решает сразу две проблемы,
// - добавляет новые заказы к списку
// - по умолчанию каждый раз, когда выполняется асинхронная операция, её прошлый результат обнуляется;
// мы же явно описали, чтобы на время загрузки возвращался прошлый результат,
// это позволяет избавиться от лоадера в UI
function* appendStrategy(next) {
const prev = yield select(state => state.asyncOperations.get(next.id));
return {
...next,
result: prev?.result && next.result
? [...prev.result, ...next.result]
: next.result || prev?.result,
};
}
class OrderService extends Service {
// пометим все методы, которые будут редактировать список нашим id
@operation(ORDERS_OPERATION_ID)
*getOrders() { ... }
@daemon()
@operation({
id: ORDERS_OPERATION_ID
updateStrategy: appendStrategy
})
*addOrder() {
const id = getNewId();
const order = {id, description: `Order ${id}`};
yield* call(addOrder, order);
// возвращаем созданный заказ, чтобы можно было его добавить к списку
return [order];
}
}
Можно написать множество переиспользуемых стратегий для типовых кейсов - добавление/удаление из списка, разного рода слияние данных и т. д.
Это позволяет сделат ь методы сервиса более легкими и читаемыми, и декларативно описать, как результат их исполнения будет обрабатываться.
Проверим наше решение