React Junior
207 subscribers
37 photos
462 links
Изучение React с нуля
加入频道
Redux-saga. Общий обзор

Быстрый экскурс в Redux-saga: что за штука, зачем нужна, как примерно работает.

Saga - это аналог Redux-thunk, штука, которая помогает нам управлять сайд-эффектами в redux. Когда мы диспатчим в хранилище какой-то экшен и хотим, чтобы при этом запрашивались с сервера какие-то данные - нам сюда.

С thunk уже разбирались: он позволяет вместо объекта экшена создавать функцию. Миддлвар смотрит на экшен и, если видит, что это функция, не пропускаего его в стор, а вызывает, передавая аргументами полезные методы и данные стора. (самописный пример)

То есть с thunk нам необходимо усложнять action creators, всю логику мы кладем туда.

Saga работает по-другому: она "прослушивает" экшены и, если видит нужный, запускает обработчик для него. Соответственно, в саге есть вотчеры (следят за экшенами) и воркеры (содержат бизнес-логику).

А еще есть эффекты - это библиотечные функции (встроены в Saga), которые позволяют нам выстроить правильную последовательность действий внутри воркеров, в общем, утилиты.

Структура saga

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

Важно: саги - это функции-генераторы! То есть они могут прерывать процесс своего выполнения. Освежить память по генераторам можно, например, здесь: https://yangx.top/furrycat/456

Установка и подключение

Нам нужен пакет redux-saga. Сага подключается к redux-хранилищу как миддлвар.

Посмотреть можно здесь: https://codesandbox.io/s/redux-saga-getting-started-react-junior-h3l596?file=/src/store/index.js

- Сначала создаем миддлвар с помощью функции createSagaMiddleware,
- добавляем его к остальным с помощью applyMiddleware,
- передаем их в createStore.
- После создания хранилища нужно еще запустить саговский миддлвар, вызвав у него метод sagaMiddleware.run().

#redux #управлениесостоянием #примерыкода #saga
👍5
Redux-saga. Структура саг и эффекты

Итак, сага создает некие процессы. Они могут быть последовательные, зависящие друг от друга, (сначала получили id пользователя, потом для этого id список постов) и параллельные, не зависящие друг от друга (получили посты и получили список уведомлений).

У нас есть корневая сага (`rootSaga`) - корневой процесс.

Сага может вызывать другие саги, формируя древовидную структуру, причем вложенные саги связаны с родительской.

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

Например, чтобы "подписаться" на экшен, нужен эффект take:


import { take } from "@redux-saga/core/effects"

function workerSaga() {
console.log('count increased')
yield;
}

function* watcherSaga() {
yield take(INCREMENT_COUNTER);
yield workerSaga();
}

function* rootSaga() {
yield watcherSaga();
}


Корневая сага запускает вотчер, который подписывается на экшен с типом INCREMENT_COUNTER. Пока этот экшен не сработает, остальной код в вотчере НЕ вызовется. То есть мы будем ждать экшена и только потом запустим воркер.

Эта штука сработает только один раз, при первом же экшене INCREMENT_COUNTER.

Если нужно, чтобы срабатывало при каждом, потребуется другой эффект - takeEvery:


function* watcherSaga() {
yield takeEvery(INCREMENT_COUNTER, workerSaga)
}


А внутри воркера можем получить доступ к хранилищу и вывести текущее значение счетчика с помощью эффекта select:


function* workerSata() {
const count = yield select((state) => state.counter.value);
console.log("count increased", count);
}


Немного усложним и представим, что воркер выполняет некую асинхронную работу, которая занимает время, например, отправляет запрос на сервер. И мы не хотим, чтобы два запроса выполнялось одновременно. Для этого есть эффекты takeLatest и takeLeading - используем их вместо takeEvery.

takeLeading не запускает воркер заново, пока выполняется предыдущий.
takeLatest отменяет текущий запрос, если пришел новый экшен.

Кстати, для создания искусственной задержки есть эффект delay.

Так, экшены отслеживать умеем, из стора читать умеем, надо еще диспатчить. Это тоже можно - с эффектом put.

Например, реализуем асинхронный инкремент. Тут нам понадобится и delay, и takeLatest, и put.


function* asyncIncrementSaga() {
yield delay(1000);
yield put({ type: INCREMENT_COUNTER });
}

function* watcher() {
yield takeLatest(INCREMENT_COUNTER_ASYNC, asynIncrementSaga)
}


Для начала, пожалуй, достаточно.

#redux #управлениесостоянием #примерыкода #saga
👍4
React: самые используемые типы

Статья (англ.): https://jser.dev/2023-05-31-react-types-in-typescript/

Автор статьи сделал подборку типов, используемых в React, чтобы нам не пришлось.

1. ReactElement

Главный "строительный" метод React - React.createElement(). Он принимает конфиг элемента, который нужно создать - его и описывает интерфейс ReactElement.


interface ReactElement {
type: T,
props: P,
key: Key | null,
}


ReactElement или JSX.Element (то же самое, просто алиас) - это то, что должно быть возвращено из JSXElementConstructor (из функционального или классового компонента).

2. ReactNode

Это "надтип" для ReactElement, как и в DOM - есть ноды (включают комментарии и текстовые узлы), а есть их подмножество - элементы.

В тип ReactNode входят ReactElement, строки, числа, булевы значения, фрагменты, порталы и даже null и undefined

3. FunctionComponent

Интерфейс, описывающий функциональный компонент

4. RefObject и MutableRefObject

Типы, описывающие рефы, созданные хуком useRef.
У useRef есть несколько перезагрузок и одна из них приводит к тому, что поле ref.current становится readonly и его нельзя изменить 😳

5. ComponentProps

Обобщенный тип, позволяющий "извлечь" из типа компонента его пропсы. Под капотом использует infer.

6. Dispatch и SetStateAction

Эти типы описывают самый популярный хук useState.
Тип Dispatch - это просто функция, которая принимает аргумент заявленного типа и ничего не возвращает.
SetStateAction - это либо новое состояние, либо функция, которая принимает старое состояние и возвращает новое.

7. События

С типизацией событий в React всегда сложно. Есть два подхода:
- типизировать само событие (SyntheticEvent), которое приходит в обработчик в виде аргумента, например, MouseEvent
- или типизировать сам обработчик - MouseEventHandler

Оба типа - дженерики, которым можно передать тип элемента, на котором происходит событие.

#typescript #ссылки
👍3
Redux-saga. Эффект call и тестирование саг

Продолжаем разбор redux-saga.

В прошлый раз мы узнали:

- что саги объединяются в виде дерева - из одной саги могут "прорастать" другие. Такая структура позволяет при ошибке изолировать проблемную ветку, об этом поговорим чуть позже.

- что саги делятся на вотчеры (подписка на события) и воркеры (обработчики событий).

- что у нас есть куча встроенных хелперов (эффектов) и для вотчеров (take/takeEvery/takeLatest/takeLeading), и для воркеров (delay/select/put). Эффекты - это обычные объекты, содержащие инструкции для миддлвара.

Теперь чуть поближе посмотрим на эффекты на примере эффекта call и немного затронем тему тестирования саг.

Call

Call - встроенный эффект, который работает как стандартный метод Function.call. Он принимает функцию, которую нужно вызывать и набор аргументов для вызова. То есть мы можем вызвать функцию напрямую:


function* fetchProducts() {
const products = yield API.fetch('/products');
}


А можем через call:


function* fetchProducts() {
const products = yield call(API.fetch, '/products');
}


Функциональной разницы нет, запрос и так, и так будет выполнен. Но есть разница техническая.
Например, мы хотим протестировать сагу fetchProducts. Это генератор, поэтому нужно использовать метод next для получения каждого промежуточного результата:


const iterator = fetchProducts();
const res = iterator.next();


В первом случае в переменной res окажется промис. Во втором - объект эффекта, который выглядит примерно так:


{
CALL: {
fn: Api.fetch,
args: ['./products']
}
}


Эффект возвращает нам просто объект с инструкциями, который проще тестировать. А сам вызов произойдет уже в миддлваре. Для тестирования мы можем просто снова создать эффект с теми же параметрами:


const { call } from 'redux-saga/effects';
const iterator = fetchProducts();
const res = iterator.next();

assert.deepEqual(
res.value,
call(API.fetch, '/products'),
"fetchProducts should yield an Effect call(API.fetch, './products')"
)


При таком подходе нам не придется подменять метод API.fetch, так как мы его даже не вызываем.

Контекст выполнения

В call можно передать контекст выполнения:


yield call([obj, obj.method], arg1, arg2, ...)


Apply

Кроме call есть еще эффект apply, который принимает аргументы в виде массива:


yield apply(obj, obj.method, [arg1, arg2, ...])


call и apply удобно использовать для функций, которые возвращают промис. Если нужно вызвать функцию в node-стиле, которая принимает последним параметром коллбэк, есть специальный эффект cps.

#redux #управлениесостоянием #примерыкода #saga
👍3🔥1
Redux-saga. Блокирующие и неблокирующие эффекты. Call vs Fork

В redux-saga есть прекрасный эффект takeEvery, который позволяет подписаться на каждый вызов события.

Логика у него довольно простая, попробуем реализовать его своими силами с помощью уже знакомых простых эффектов take и call:


function* worker() {
yield delay(1000);
yield console.log('click');
}

function* watcher() {
while (true) {
yield take('CLICK');
yield call(worker);
}
}


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

Но этот код будет работать не так, как предполагается. Догадались, почему?

Эффект call является блокирующим. Это значит, что когда он запускается, функция-генератор приостанавливает свою работу, ждет, когда он выполнится, и только потом возобновляет свое выполнение.

- произошло событие CLICK
- запустилась сага worker
- выполнение worker занимает некоторое время, так как там тоже есть блокирующий эффект delay
- когда worker выполнился (минимум через 1 секунду), продолжается выполнение watcher - цикл идет на следующий круг

Если событие CLICK поступит во время выполнения саги worker, его просто никто не заметит. То есть логика эффекта takeEvery не повторяется.

Чтобы это починить, нужно заменить блокирующий эффект call на неблокирующий fork. Он создаст отдельную "ветку" для новой саги, а выполнение родительской саги сразу же продолжится.


function* watcher() {
while (true) {
yield fork('CLICK');
yield call(worker);
}
}


Демонстрация разницы: https://codesandbox.io/s/redux-saga-call-vs-fork-react-junior-8yhf4l?file=/src/app/saga.js

Важно понять, что с неблокирующими эффектами мы не можем использовать, например, блок try-catch, так как выполнение неблокирующей саги происходит вне контекста родительской саги - в "конкурентном" режиме.

#redux #управлениесостоянием #примерыкода #saga
👍4
Redux-saga. Отмена эффекта

Теперь попробуем повторить поведение встроенного эффекта takeLatest. Помимо того, что нам нужно отслеживать событие и запускать для него воркер, необходимо еще останавливать ранее запущенные воркеры. Для этого есть эффект cancel.

Помним, что все функции эффектов возвращают простой объект эффекта. Именно этот объект и нужно передать в cancel, чтобы отменить эффект.


let unique = 1;

export function* clickWorker(counter) {
yield delay(1000);
yield console.log("click", counter);
}

export function* clickWatcher() {
let lastEffect;

while (true) {
yield take("CLICK");
if (lastEffect) {
yield cancel(lastEffect);
}
lastEffect = yield fork(clickWorker, unique1++);
}
}


Демо здесь: https://codesandbox.io/s/redux-saga-cancel-react-junior-hlx6hh?file=/src/app/saga.js

#redux #управлениесостоянием #примерыкода #saga
👍3
Redux-saga. Отмена эффекта (продолжение)

Мы научились запускать неблокирующий эффект fork и затем при необходимости отменять его с помощью cancel. Но что если до отмены наша форкнутая сага успела что-то сделать, например, отправила запрос на сервер. При отмене саги было бы хорошо отменить и этот запрос.

Пример из документации - фоновое сохранение данных, например, текста поста в админке вашего сайта. При событии START_BACKGROUND_SYNC запускается процесс синхронизации - запрос отправляется раз в какое-то время. При событии STOP_BACKGROUND_SYNC нужно этот процесс остановить, а также отменить последний отправленный запрос.

Для этого в форкнутой саге нужно использовать эффект cancelled + try-finally.


function* bgSync() {
try {
while (true) {
// получаем нужные данные/отправляем нужные запросы
// повторяем каждые 5 секунд
yield delay(5000)
}
} finally {
// блок сработает, если сага была отменена
if (yield cancelled())
// логика отмены
}
}

function* main() {
// ждем события начала синхронизации
while ( yield take('START_BACKGROUND_SYNC') ) {

// форкаем сагу с логикой синхронизации
const bgSyncTask = yield fork(bgSync)

// ждем события конца синхронизации
yield take('STOP_BACKGROUND_SYNC')

// отменяем синхронизацию
yield cancel(bgSyncTask)
}
}


Эффект cancel останавливает работу генератора, поэтому он перепрыгивает сразу в блок finally. Тут мы и проверяем, вызвано ли завершение работы тем, что произошла отмена.

#redux #управлениесостоянием #примерыкода #saga
🔥3
Redux-saga. Прикрепление саг к родителю

Наконец-то дошли до архитектуры - посмотрим, зачем нам нужно такое дерево саг.

Мы уже знакомы с эффектом fork, который позволяет запускать новую ветку саг, не блокируя выполнение родителя. А есть еще spawn. В чем между ними разница?

- fork (от слова 'вилка') создает новую ветку, которая "прикреплена" (attached) к родителю.
- spawn (от слова 'порождать') создает новую "отделенную" (detached) ветку.

То есть разница заключается в наличии связи с "родительской" сагой, которая создала новую ветку.

- Сага является завершенной, если завершено ее собственное тело, а также все прикрепленные к ней ветки.
- Если в прикрепленной ветке возникает ошибка, то она всплывает вверх - к родительской саге (и дальше, если ее не обработать).
- При отмене саги с помощью эффекта cancel, отменяются также все прикрепленные к ней ветки.

Важно: обработка ошибки в блоке try-catch возможна только для блокирующих вызовов (эффект call`). Отдельные ветки (`fork`, `spawn`) выполняются асинхронно, поэтому с ними такое не сработает.

Ветки, созданные с помощью spawn, не имеют никакой связи со своим родителем и похожи больше на отдельные "корневые" саги - таким образом создаются новые ветки в эффекте takeEvery.

#redux #управлениесостоянием #saga
2🔥1
Redux-saga. Композиция саг

👉 All

Чтобы запустить несколько саг параллельно, используем эффект all:


const results = yield all([call(task1), call(task2), ...]);
yield put(showResults(results));


Идея такая же, как в Promise.all - дожидаемся, пока все саги в массиве выполнятся, и продолжаем исполнять код дальше.

👉 Race

Есть и аналог Promise.race - эффект race, но с немного другим синтаксисом:


const {posts, timeout} = yield race({
posts: call(fetchApi, '/posts'),
timeout: delay(1000)
})

if (posts)
yield put({type: 'POSTS_RECEIVED', posts})
else
yield put({type: 'TIMEOUT_ERROR'})


Тут мы ждем, пока выполнится запрос, но не дольше одной секунды. Если запрос успеет выполниться, то его результаты придут в переменную posts.

Удобно сочетать race с эффектом cancel, чтобы отменять "проигравшие" гонку саги.

Не следует использовать эффект fork внутри race, так как он неблокирующий и поэтому всегда будет "выполняться" первым.

#redux #управлениесостоянием #примерыкода #saga
🔥1
Redux-saga. Корневая сага

Еще пару слов об организации корневой саги с учетом новых знаний про блокирующие/неблокирующие эффекты, а также про attached/detached ветки.

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

🔹Параллельный запуск с all


function* rootSaga() {
yield all([
saga1(),
saga1()
])
}


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

🔸Неблокирующие вызовы с fork


yield fork(saga1)
yield fork(saga2)
yield fork(saga3)


🔹All + fork

Чтобы решить проблему блокирования корневой саги эффектом all, можно сочетать его с fork, чтобы вызовы всех саг были неблокирующими:


yield all([ fork(saga1), fork(saga2), fork(saga3) ])


Так все саги запустятся параллельно, но при этом all не будет ждать, пока все они выполнятся.

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

🔸Отделенные ветки со spawn

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


yield spawn(saga1)
yield spawn(saga2)
yield spawn(saga3)


🔹Перезапуск при падении

И еще один примерчик на сладкое - возможность перезапуска саги, если она не завелась:


const sagas = [
saga1,
saga2,
saga3,
];

yield all(sagas.map(saga =>
spawn(function* () {
while (true) {
try {
yield call(saga)
break
} catch (e) {
console.log(e)
}
}
}))
);


Ветки создаются с помощью spawn, чтобы не влиять на родителя и не блокировать его выполнение.
Каждая дочерняя сага запускается с помощью блокирующего эффекта call, поэтому мы можем использовать блок try-catch для обработки ошибок. Если сага удачно запустилась, то на этом все и заканчивается, если же нет, то происходит новый виток бесконечного цикла while(true) и выполняется новая попытка.

В целом все эти примеры актуальны не только для корневой саги, но и для любых других.

#redux #управлениесостоянием #примерыкода #saga
👍2
Redux-saga. ActionChannel

Эффект takeEvery позволяют ловить и обрабатывать каждый экшен - все обработчики выполняются параллельно.

take + fork делает то же самое, а take + call позволяет игнорировать новые события, пока не будет обработано предыдущее.

А что делать, если мы не хотим пропускать события, но при этом необходимо обрабатывать их последовательно, а не параллельно?

Для этого в саге есть эффект actionChannel - канал экшенов. Нужно создать канал, а потом подписаться на него (а не на сам экшен). Канал будет буферизировать поступающие экшены, пока не обработан предыдущий.


function* watchRequests() {
// создаем канал
const requestChan = yield actionChannel('REQUEST')

while (true) {
// подписываемся на него
const {payload} = yield take(requestChan)

// обрабатываем экшен
yield call(handleRequest, payload)
}
}


Тут важно использовать именно call для обработки, так как она должна быть блокирующей.

#redux #управлениесостоянием #примерыкода #saga
👍2
Redux-saga. EventChannel

ActionChannel из прошлого поста предназначен для работы с redux-стором и обычными экшенами - он просто особым образом их обрабатывает.

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

Создается такой канал с помощью функции eventChannel:


import { eventChannel, END } from 'redux-saga'

function createCountdownChannel(seconds) {
const channel = eventChannel(function(emit) {
const interval = setInterval(function() {
seconds--;

if (seconds > 0) emit(seconds);
else emit(END);
}, 1000);

return function() {
clearInterval(interval);
}
})

return channel;
}


В eventChannel нужно передать функцию, которая будет подписываться на источник событий. В качестве аргумента она получает эмиттер - функцию, которая будет диспатчить экшены в канал событий.

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

Чтобы закрыть канал (если события перестали поступать, нужно использовать константу `END`).

Функция-подписчик должна вернуть метод для сброса эффектов (как в хуке useEffect в React).

Использование канала событий:


export function* saga() {
const channel = yield call(createCountdownChannel, 10)

try {
while (true) {
let seconds = yield take(channel)
console.log(`countdown: ${seconds}`)
}
} finally {
console.log('countdown terminated')
}
}


Канал можно закрыть и извне, вызвав его метод close:


channel.close()


#redux #управлениесостоянием #примерыкода #saga
👍2
Redux-saga. Простые каналы

У нас уже были каналы для экшенов из стора (ActionChannel), каналы для событий из внешних источников (EventChannel).

И в дополнение ко всему этому есть самые простые каналы - Channel. Их отличие в том, что изначально они ни на что не подписаны, ни на стор, ни на внешний источник событий. Это совершенно пустой канал, в котором нет ни одного события.

Одна сага может его создать, другая сага может на него подписаться. Пушить события в него нужно вручную.

Зачем все это нужно?

Документация предлагает нам следующий кейс: с текущими инструментами мы можем либо обрабатывать экшены последовательно, не начиная новый, пока не закончится предыдущий (`take`), или же параллельно все (`takeEvery`). Но не можем, например, обрабатывать параллельно только 3 экшена. И пока не закончится обработка хотя бы одного из нех, не начинать следующий.

Вот для этого нам и нужны каналы.


import { channel } from 'redux-saga'

function* watcher() {
const chan = yield call(channel); // создаем канал

// подписываемся на этот канал три раза
yield fork(handleRequest, chan);
yield fork(handleRequest, chan);
yield fork(handleRequest, chan);

while (true) {
// ловим экшен REQUEST и кладем в канал
const {payload} = yield take('REQUEST')
yield put(chan, payload)
}
}

function* handler(chan) {
while (true) {
// ловим событие из канала
const payload = yield take(chan)
...
}
}


Что тут происходит?

Мы создаем канал, создаем три слушателя для этого канала. Затем как обычно ловим нужный экшен и просто кладем его в канал.
При попадании экшена в канал, его получит первый свободный слушатель (если свободны несколько, получит все равно только один). Если все слушатели уже заняты, событие останется висеть в канале до тех пор, пока кто-то не освободится.

#redux #управлениесостоянием #примерыкода #saga
👍2
Redux-saga. Multicast-каналы

И наконец в дополнение ко всем уже разобранным каналам у нас есть multicast-каналы. Они работают так же, как и обычные каналы (channel), только уведомление о поступившем событии получают ВСЕ подписчики.

Таким образом, на одно событие можно подписать несколько обработчиков с разной логикой.


import { multicastChannel } from 'redux-saga'

function* watchRequests() {
const channel = yield call(multicastChannel)

// подписываем разные обработчики
yield fork(handler1, channel)
yield fork(handler2, channel)

while (true) {
const { payload } = yield take('REQUEST')
yield put(channel, payload)
}
}


#redux #управлениесостоянием #примерыкода #saga
👍2
Redux-saga. Кастомный I/O

Обычно мы подключаем саги к стору приложения с помощью миддлвара. Эффекты take и put связываются со стором и слушают его/диспатчат в него новые экшены.

Но можно запустить сагу отдельно от стора, если это необходимо.

Для этого нам нужно создать кастомный "стор" с полями
- channel,
- dispatch
- и getState.

То есть у этого стора должен быть канал, в который будут поступать события - его создаем с помощью функции stdChannel. Этот канал нужно соединить с внешним I/O.

Для запуска вызываем функцию runSaga и передаем ей новый стор и собственно сагу.


import { runSaga, stdChannel } from 'redux-saga'

const emitter = new EventEmitter()
const channel = stdChannel()
emitter.on("action", channel.put)

const myIO = {
channel,
dispatch(output) {
emitter.emit("action", output)
},
getState() {
return state
}
}

runSaga(
myIO,
function* saga() { ... },
)


#redux #управлениесостоянием #примерыкода #saga
👍2
Redux-saga. Рецепты

На страничке Recipes https://redux-saga.js.org/docs/recipes в документации redux-saga есть несколько полезных сниппетов:

- throttling
С использованием встроенного эффекта throttle.

- debouncing
Сохраняем активный воркер и отменяем его (`cancel`), если пришло новое событие, либо используем встроенный эффект takeLatest.

- повторное выполнение неудачных XHR-запросов
С использованием встроенного эффекта retry или комбинацией базовых эффектов call, take, delay и блока try-catch.

- функционал Отменить последнее действие
С помощью spawn и race.

- batching нескольких экшенов
С использованием библиотеки redux-batched-actions.

#redux #управлениесостоянием #документация #saga
👍3
Forwarded from TypeScript Challenge
Type Challenges #11. Tuple to object

Ссылка на задачу

Условие

Нужно превратить массив в объект, ключами в котором являются элементы исходного массива.

Решение


type TupleToObject<T extends ReadonlyArray<string | number | symbol>> = {
[P in T[number]]: P
}


Прежде всего, уточняем, что ключами объекта могут быть только строки/числа/символы. Вместо перечисления можно использовать встроенный тип PropertyKey:


type TupleToObject<T extends readonly PropertyKey[]> = {
[P in T[number]]: P
}


Для получения набора элементов массива используем конструкцию T[number].

#easy
👍6
Forwarded from TypeScript Challenge
Type Challenges #189. Awaited

Ссылка на задачу

Условие

Предположим, что у нас есть тип, завернутый в другой тип, например, Promise<string>. Нужно написать утилиту MyAwaited для получения внутреннего типа.

Решение

Чтобы решить задачку, нужно понять, какая именно обертка может быть. Посмотрим на тестовые кейсы:


type X = Promise<string>
type Y = Promise<{ field: number }>
type Z = Promise<Promise<string | number>>
type Z1 = Promise<Promise<Promise<string | boolean>>>
type T = { then: (onfulfilled: (arg: number) => any) => any }

type cases = [
Expect<Equal<MyAwaited<X>, string>>,
Expect<Equal<MyAwaited<Y>, { field: number }>>,
Expect<Equal<MyAwaited<Z>, string | number>>,
Expect<Equal<MyAwaited<Z1>, string | boolean>>,
Expect<Equal<MyAwaited<T>, number>>,
]


Тут обычные промисы, а также кастомная структура с полем then. Кроме того, внутри промиса может быть другой промис и так далее.

Для описания обертки нам подойдет встроенная утилита PromiseLike<T>, осталось только вывести вложенный тип с помощью infer:


type MyAwaited<T> = T extends PromiseLike<infer A> ? A : never;


Но не забываем, что внутри обертки может быть еще одна обертка, поэтому нужно добавить немножко рекурсии:


type MyAwaited<T> = T extends PromiseLike<infer A> ? (A extends PromiseLike<infer B> ? MyAwaited<A> : A) : never;

#easy
👍2
Redux ToolKit: краткий конспект

Собираюсь повторить RTK Query (а затем разобраться с React Query). Поэтому краткая выжимка по Redux Toolkit:

1. Мыслим слайсами

Функция createSlice создает сразу и action creators, и редьюсер, и даже набор селекторов, если мы работаем с EntityAdapter.

2. Асинхронщина

Вся асинхронщина через createAsyncThunk + extraReducers.

3. Селекторы

Селекторы напрямую в RTK не входят, для них используем reselect.

4. Связь с компонентами

В компонентах используем классические хуки useSelector и useDispatch.

5. Создание хранилища

Стор создается с помощью configureStore.

***

В целом это все, в основном удобный синтаксический сахар, ничего сверхъестественного поверх базового Redux нет.

#управлениесостоянием #redux
👍5
Forwarded from TypeScript Challenge
Type Challenges. Уровень easy. Резюме

Итак, мы прорешали 13 задачек уровня easy.

4 - Pick
7 - Readonly
11 - Tuple to Object
14 - First of Array
18 - Length of Tuple
43 - Exclude
189 - Awaited
268 - If
533 - Concat
898 - Includes
3057 - Push
3060 - Unshift
3312 - Parameters

Проведем небольшую ретроспективу и отметим основные моменты решений.

🟢 1. extends

Самое популярное ключевое слово в решениях.

Используется для уточнения типов в дженериках (9 задач):


type MyPick<T, K extends keyof T>


Используется для сравнения в условных типах (2 задачи):


type If<C extends boolean, T, F> = C extends true ? T : F


Используется в комбинации с ключевым словом infer для выведения одних типов из других (4 задачи):


type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never


🟢 2. Копирование ключей объекта (3 задачи)

Используется для создания Mapped Types, когда новый тип повторяет ключи исходного


type MyReadonly<T> = {
readonly [K in keyof T]: T[K]
}


🟢 3. keyof

Ключевое слово для получения набора из всех ключей объекта (2 задачи)


type MyPick<T, K extends keyof T> = {
[P in K]: T[P]
}


🟢 4. Превращение массива типов в перечисление типов(1 задача)

Используем конструкцию T[number].


type TupleToObject<T extends ReadonlyArray<string | number | symbol>> = {
[P in T[number]]: P
}


🟢 5. Использование readonly структур для работы с кортежами (3 задачи)


type Length<T extends readonly any[]> = T['length']


🟢 6. Деструктуризация массивов (5 задач)


type Push<T extends any[], U> = [...T, U]


#easy
👍4
RTK Query

Освежаю знания об RTK Query

RTK Query - это часть Redux Toolkit, которая предназначена для удобной работы с API. Она собирает все, что относится к API в одном месте и дает нам много приятного синтаксического сахара.

Заводим для API отдельный слайс, где будут лежать все данные, полученные с сервера. Но не обычный слайс, который createSlice, а прокачанный слайс, который createApi.
Подключить этот api-слайс к стору посложнее, чем обычный, тут нужно и редьюсер добавить, и специальный миддлвар.

В api-слайсе определяем функцию, которая будет осуществлять запросы - baseQuery.

На этом техническая подготовка заканчивается и начинается собственно логика обмена данными - поле endpoints. Тут мы получаем builder и с его помощью (`builder.query`) создаем массив наших эндпоинтов (есть много разных настроек) . Каждый эндпоинт на выходе из слайса создает себе личный хук , который можно дернуть в любом месте приложения. А в этом хуке уже предусмотрена куча удобных поле - data, isLoading, error и т.п. И дополнительно можно закинуть ряд настроек, вроде селектора . То есть вытаскивать данные из API со всеми удобствами мы уже можем. Можно даже сочетать все это дело с EnitityAdapter.

Примечание: можно выполнить запрос и «вручную» без использования хука.

Отправка данных на сервер работает практически так же, как и вытягивание, только эндпоинт нужно немного по-другому оформить (через `builder.mutation`). Хук тоже немного другой.

Самое приятное, что запросы к серверу кешируются, однако кеш иногда приходится сбрасывать. Это можно сделать напрямую руками, с помощью метода refetch, который есть в хуках. Но лучше настроить зависимости с помощью системы тегов, чтобы RTK Query сбрасывала то, что нужно, самостоятельно.

Конечно, api-слайс можно расчленять (injectEndpoints).

RTK Query предлагает продвинутые техники работы с API, например, оптимистичное обновление, когда данные меняются сразу, а запрос выполняется уже в фоне. Еще есть возможность подписаться на источник событий (сокет-канал) , чтобы получать данные из него.

Предыдущее резюме по RTK Query, с большим количество технических подробностей.

#управлениесостоянием #rtkquery #обменданными
🔥3👍2