React Junior
207 subscribers
37 photos
462 links
Изучение React с нуля
加入频道
Повторение по селекторам

Статья в документации: https://redux.js.org/usage/deriving-data-selectors

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

Зачем нужны селекторы

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

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

Организация селекторов

Документация рекомендует давать функция-селекторам осмысленные имена, начинающиеся с select.

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

Селекторы применяются вместе с хуком useSelector.

Проблемы

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

Хук useSelector сравнивает результат выполнения селектора с предыдущим (используя строгое равенство `===`). Если значения отличаются, выполняется перерендер. То есть селектор не должен возвращать новое значение, если ничего не изменилось. Но тут появляется проблема, например, с методами массивов (map, filter), которые всегда возвращают новый массив. Даже если список отфильтрованных элементов не изменился, хук увидит новый массив и посчитает, что нужен перерендер.

Чтобы избежать этого, нужна мемоизация селекторов. (Мемоизация не нужна, если селектор возвращает примитивное значение).

Мемоизация

Для мемоизации мы используем библиотеку Reselect (есть и альтернативы, которые указаны в статье). Она предоставляет функцию createSelector, которая принимает любое количество входных селекторов (для проверки изменения различных частей состояния) и один выходной (собственно для выборки данных). Выходной селектор вызывается только в том случае, если входные селекторы возвращают изменившиеся результаты.

#управлениесостоянием #документация #redux #оптимизация
👍3
Реализация функциональности Шаг назад

Статья в документации: https://redux.js.org/usage/implementing-undo-history

В статье описано, как реализовать в Redux-приложении функционал Шаг назад/вперед. В Redux это особенно просто, так как все состояние хранится в одном объекте и при каждом изменении оно представлено новым объектом - значит легко хранить последовательность состояний. К тому же вся логика управления состоянием находится в функции (редьюсере). Здесь можно использовать паттерн декоратор, чтобы добавить новый функционал, не ломая основной.

Глобально паттерн выглядит так:


{
past: [T, T],
present: T,
future: [T]
}


Текущее состояние хранится в поле present. При каждом изменении старое состояние добавляется в массив past, а present изменяется. Если потребуется сделать Шаг назад, мы просто возьмем последнее состояние из past и установим его в present. При этом текущее состояние нужно сохранить в future, чтобы к нему можно было вернуться.

В статье дана полная реализация алгоритма. Также можно воспользоваться готовой реализацией из пакета redux-undo.

#управлениесостоянием #redux #документация #паттерны
👍3
Стоит ли использовать 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://yangx.top/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://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
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