React Junior
207 subscribers
37 photos
462 links
Изучение React с нуля
Download Telegram
Стоит ли использовать Redux в Next.js-приложении?

Статья (англ.): https://javascript.plainenglish.io/should-you-use-redux-in-next-js-5e57201c34da

Автор статьи считает, что нам нужно отказаться от Redux в Next.

Причина #1: В нем нет особого смысла, так как благодаря серверному рендерингу мы сразу же, еще до рендеринга страницы, получаем нужные данные.

Причина #2: У Redux есть ряд альтернатив - более легких, нативных и удобных. Например, React Context или Local Storage. В большинстве случаев их функциональности более чем достаточно. Для более сложных сценариев, например, для загрузки данных на клиенте есть библиотеки вроде swr или react-query.

Причина #3: Redux внутри Next сложно хорошо настроить и оптимизировать.

#nextjs #ссылки #redux
👍2
Как настроить Redux в NextJS

Совсем недавно мы видели мнение, что Redux в Next.js использовать не стоит: https://t.me/c/1218235935/551

Но если вам все-таки хочется, то вот руководство по настройке (англ.): https://medium.com/how-to-react/how-to-setup-redux-in-nextjs-5bce0d82b8de

Выглядит не особо сложно. Помимо обычных настроек Redux нам понадобится еще функция createWrapper из пакета next-redux-wrapper.

const initialState = {};
const middleware = [thunk];
const store = createStore(
rootReducer,
initialState,
composeWithDevTools(applyMiddleware(...middleware))
);

const wrapper = createWrapper(function() {
return store;
});


Теперь нужно обернуть приложение в провайдер хранилища. Сделать это лучше всего в файле _app.js. В провайдер передаем store.

А чтобы все работало, нужно использовать компонент высшего порядка wrapper.withRedux.

import { Provider } from "react-redux";

function MyApp({ Component, pageProps }) {
return (
Provider store={store}
Component {...pageProps}
)
}

export default wrapper.withRedux(MyApp);


Вот и все, теперь можно пользоваться плюшками Redux.

#nextjs #redux #статьи
👍3
Redux-saga. Общий обзор

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

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

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

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

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

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

Структура saga

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

Важно: саги - это функции-генераторы! То есть они могут прерывать процесс своего выполнения. Освежить память по генераторам можно, например, здесь: https://t.me/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
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
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
React Query vs RTK Query: всестороннее сравнение

Статья (англ.): https://www.frontendmag.com/insights/react-query-vs-rtk-query/

В статье сравниваются два популярных решения для взаимодействия React-приложений с удаленными источниками данных.

Сходство

🔹Оба инструмента используют лучшие performance-практики работы с удаленными данными:

- кеширование запросов
- предотвращение дублирующихся запросов
- повторные запросы в случае ошибок

🔹Оба инструмента предлагают решения для самых распространненных задач получения данных:

- пагинация - обновление списка данных
- оптимистические обновления

Отличие

Отличие заключается в архитектуре инструментов.

🔸React Query - это независимая библиотека. Ее функциональность реализована в виде хуков, то есть привязана к жизненному циклу React-компонентов.

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

Кривая обучения

Автор считает, что и React Query, и RTK Query интуитивно понятны. Это довольно спорное утверждение, так как чтобы понять и то, и другое, требуется немного отформатировать свое понимание работы с данными.

Что касается RTK Query, тут вообще требуется сначала разобраться с Redux Toolkit - а это дело непростое.

Когда что использовать (привет, кэп)

RTK Query лучше использовать, если:

- В приложении уже используется Redux и Redux Toolkit. Тогда RTK Query доступна из коробки и уже интегрирована с прочими инструментами.
- Требуется сложная логика управления данными. RTK Query - это более мощный инструмент и на нем можно построить более сложную систему.

React Query лучше использовать, если:

- В приложении не используется глобальное состояние или запросы данных с ним не взаимодействуют. React Query не предоставляет никаких способов создания глобального стейта или взаимодействия с ним, все только внутри компонента.
- Требуется простая логика обработки данных.
- Не хочется долго разбираться.
- Не хочется тянуть зависимости в виде Redux Toolkit.

#управлениесостоянием #redux
👍1