React Junior
207 subscribers
37 photos
462 links
Изучение React с нуля
加入频道
Next.js. Продвинутые опции - 2

Режим предпросмотра

Представим, что у нас есть статическая страница, которая использует getStaticProps и/или getStaticPaths и рендерится во время сборки. Например, это страница отдельного поста.
Теперь представим, что у нас есть система управления контентом, в которой мы пишем новый пост. Хотелось бы иметь возможность увидеть, как этот пост в итоге будет выглядеть - то есть иметь превью.
То есть мы хотим использовать статическую страничку поста, но динамически, передав ей текущий черновик.
Next такое может, об этом вот здесь: https://nextjs.org/docs/advanced-features/preview-mode

Автоматическая статическая оптимизация (оптимизация статических страниц)

Этот раздел про то, что Next автоматически генерирует html для страниц, которые не используют getServerSideProps.
Тут важно помнить, что если это, например, динамический роут, то страница при рендеринге не получит параметры роута, а после гидратации получит и перерендерится.
Отследить, готовы ли параметры роута, можно, проверив поле router.isReady (хук useRouter).
Подробнее: https://nextjs.org/docs/advanced-features/automatic-static-optimization

Директория src

Фронтендерам привычно использовать для исходников директорию src. В Next же вместо этого мы размещаем все в корне проекта. При желании, можно использовать src, фреймворк это поддерживает. Тогда страницы должны лежать в папке src/pages.
Файлы конфигурации и директория public при этом должны оставаться в корне проекта.
https://nextjs.org/docs/advanced-features/src-directory

Непрерывная интеграция и кэширование сборок

Это о том, как настроить CI workflow, чтобы кэш сохранялся между сборками: https://nextjs.org/docs/advanced-features/ci-build-caching

Multi zones

Словом zone (зона?) обозначается отдельное развертывание (деплой) Next-приложения. Таких зон может быть несколько и они могут объединяться в одно приложение.
Не совсем пока понятно, что это и для чего.
Подробнее: https://nextjs.org/docs/advanced-features/multi-zones

Измерение производительности

Next.js самостоятельно собирает все метрики производительности приложения. Чтобы использовать их, нужно из файла pages/_app.js экспортировать функцию reportWebVitals. Она будет вызываться для каждой измеренной метрики.
Подробнее: https://nextjs.org/docs/advanced-features/measuring-performance

Трассировка выходных файлов

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

Подробнее: https://nextjs.org/docs/advanced-features/output-file-tracing

#nextjs #документация
👍5
Next.js. Middleware

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

Создать такой миддлвар очень просто - нужно просто сделать файлик middleware.js и положить его в корень проекта (рядом с папкой pages). Из этого файла нужно экспортировать функцию middleware.

В качестве параметра функция принимает объект запроса и может либо вернуть объект ответа (NextResponse), либо куда-нибудь перенаправить. Если ничего не вернуть, то запрос выполнится без всяких изменений, как будто и нет никакого миддлвара.

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

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

Думаю, к этой теме мы еще вернемся, звучит она интересно.

Подробнее: https://nextjs.org/docs/advanced-features/middleware

#nextjs #документация
🔥3👍1
Next.js. Обработка ошибок

Статья в документации: https://nextjs.org/docs/advanced-features/error-handling

Тут ничего особо интересного:

development mode

В режиме разработки приложение будет просто выбрасывать модалку с описанием ошибки.

Ошибки сервера

Если ошибка произошла на стороне сервера, пользователь увидит страницу 500 (или 404, если нужная страница не найдена).

Ошибка клиента

Для обработки клиентских ошибок нам предлагают использовать предохранитель ErrorBoundary. Импортируем его в _app.js и оборачиваем в него компонент страницы.

#nextjs #документация
👍4
#css #tip by reactive_dude

Полезный кейс использования React key
👍3
Так как я сейчас нахожусь в процессе поиска новой работы, в ближайшее время на втором канале https://yangx.top/furrycat будет несколько постов про подготовку к собеседованию по JS/React/CSS и пр. веб-технологиям - быстренько повторим основы 😌
👍2🔥1
Forwarded from Cat in Web
Материалы для подготовки к интервью

Собираю все, что набралось во время подготовки:

Самые основы JS

Типы данных https://yangx.top/furrycat/440
Объектная обертка - https://yangx.top/furrycat/450
Set/Map/WeakSet/WeakMap - https://yangx.top/furrycat/453
use strict https://yangx.top/furrycat/443
Логические операторы https://yangx.top/furrycat/439
var/let/const - https://yangx.top/furrycat/454
Хойстинг https://yangx.top/furrycat/442
Стрелочные функции - https://yangx.top/furrycat/455
Symbol - https://yangx.top/furrycat/457
Мутабельность и иммутабельность - https://yangx.top/furrycat/475
Типизация - https://yangx.top/furrycat/473

Основы посложнее

Область видимости, замыкание https://yangx.top/furrycat/441
Побитовые операторы - https://yangx.top/furrycat/458
Итераторы и генераторы - https://yangx.top/furrycat/456
Регулярные выражения - https://yangx.top/furrycat/459
Каррирование - https://yangx.top/furrycat/497

ООП, объекты

this - https://yangx.top/furrycat/444
Прототип - https://yangx.top/furrycat/445
Конструкторы - https://yangx.top/furrycat/446
Прототипное наследование - https://yangx.top/furrycat/447
instanceof - https://yangx.top/furrycat/448
Классы - https://yangx.top/furrycat/449
Объекты и их свойства - https://yangx.top/furrycat/452

Асинхронность

Асинхронность - https://yangx.top/furrycat/451
Event loop - https://yangx.top/furrycat/463

Работа в браузере

DOM - https://yangx.top/furrycat/461
События - https://yangx.top/furrycat/462
Рендеринг страницы - https://yangx.top/furrycat/470
Метрики производительности - https://yangx.top/furrycat/471
Оптимизация страницы - https://yangx.top/furrycat/472

Сети

Сети - https://yangx.top/furrycat/464
CORS - https://yangx.top/furrycat/465
Same origin policy - https://yangx.top/furrycat/466
Запросы к серверу: AJAX, JSONP - https://yangx.top/furrycat/467
Постоянная связь с сервером - https://yangx.top/furrycat/468
REST - https://yangx.top/furrycat/469

React

React. Основы - https://yangx.top/furrycat/477
React. Рефы - https://yangx.top/furrycat/478
React. Контекст - https://yangx.top/furrycat/479
Оптимизация в React - https://yangx.top/furrycat/480
Хуки - https://yangx.top/furrycat/481

Алгоритмы

Анализ сложности алгоритмов - https://yangx.top/furrycat/487
Популярные алгоритмы сортировки - https://yangx.top/furrycat/494
Алгоритмы в JS - https://yangx.top/furrycat/495

Паттерны проектирования

Паттерны проектирования - https://yangx.top/furrycat/498
Порождающие паттерны - https://yangx.top/furrycat/496
Структурные паттерны - https://yangx.top/furrycat/499

Прочее

Прогрессивное улучшение и изящная деградация - https://yangx.top/furrycat/474
SPA и PWA - https://yangx.top/furrycat/476
Функциональное программирование - https://yangx.top/furrycat/460

Примеры собесов и советы

Фронтендер в Яндекс - https://yangx.top/furrycat/486
Джун - https://yangx.top/furrycat/488
Миддл - https://yangx.top/furrycat/489
Еще миддл - https://yangx.top/furrycat/493
Алгоритмы - https://yangx.top/furrycat/502

#интервью
🔥53👍2
Next.js. Индикатор загрузки страницы

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

По мотивам статьи How To Handle Loading Between Page Changes in Next.js :) (англ.) https://medium.com/@remoteupskill/how-to-manage-loading-elegantly-in-your-next-js-application-5debbfb4cace

Нам потребуется каким-то образом отслеживать начало и окончание перехода между страницами. Логично, что для этого пригодится объект роутера, который можно получить из хука useRouter. У роутера есть поле router.events - это эмиттер событий. Нам нужно подписаться на событие начала перехода (routeChangeStart) и событие окончания перехода (routeChangeComplete).

Инкапсулируем логику отслеживания в виде хука useLoading. Он будет возвращать состояние isLoading, которое будет меняться в зависимости от событий роутера.

Подписка происходит внутри хука useEffect, не забываем также про сброс эффекта.

import { useEffect, useState } from "react";
import { useRouter } from "next/router";

function useLoading() {
const router = useRouter();
const [isLoading, setLoading] = useState(false);

useEffect(function() {
function onStart() {
setLoading(true);
}
function onComplete() {
setLoading(false);
}

router.events.on("routeChangeStart", onStart);
router.events.on("routeChangeComplete", onComplete);

return () => {
router.events.off("routeChangeStart", onStart);
router.events.off("routeChangeComplete", onComplete);
};
}, [router]);

return isLoading;
}


Теперь можно создать компонент спиннера и вывести/скрыть необходимый индикатор в зависимости от состояния isLoading.

const isLoading = useLoading();

if (isLoading) return "Loading...";

return null;


Демо: https://codesandbox.io/p/sandbox/next-page-loader-react-junior-r4n00r?file=%2Fhooks%2FuseLoading.ts&selection=%5B%7B%22endColumn%22%3A7%2C%22endLineNumber%22%3A11%2C%22startColumn%22%3A7%2C%22startLineNumber%22%3A11%7D%5D

В демо есть две странички - Home и About. Для каждой добавлена искусственная задержка загрузки (2 секунды) в getServerSideProps, чтобы виден был индикатор.

Отдельного компонента индикатора нет, он размещен вместе с прочей общей разметкой в файле _app.tsx.

#ссылки #nextjs #примерыкода
👍2🔥2
Стоит ли использовать 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
Next.js Middleware: Как оно работает и 5 примеров использования

Статья (англ.): https://javascript.plainenglish.io/next-js-middleware-how-it-works-and-5-real-use-cases-cfacbeb810c9

Про миддлвары в Next мы уже начинали говорить вот тут: https://yangx.top/react_junior/504

В качестве миддлвара выступает одна простая функция middleware, которая должна экспортироваться из файла middleware.js в корне проекта.

В качестве параметра она получает запрос пользователя (request) и может редиректнуть его, если необходимо.

import { NextResponse } from 'next/server';

export function middleware(request) {
// redirect
return NextResponse.redirect(new URL('new_url', request.url));

// rewrite (сохранить урл, но показать другую страницу)
const url = request.nextUrl.clone();
url.pathname = 'new_url';
return NextResponse.rewrite(url);

// ничего не изменять
return NextResponse.next();
}

Из того же файла можно экспортировать переменную config, в которой, например, можно определить для каких урлов применяется миддлвар (config.matcher).

export const config = {
matcher: '/dashboard/:path*'
}


В статье разбирается базовый функционал миддлвара, а также несколько кейсов его использования:

- аутентификация: получаем данные авторизации из заголовка запроса (request.headers.get('authorization')), если они не совпадают с нужными, переадресуем на страницу авторизации.

- страница "Сайт находится в разработке": если в урле есть секретный параметр, то показываем реальную страницу, если нет - переадресуем всех на страницу "В разработке".

- персонализация: делим пользователей на группы, по запросу определяем, к какой группе принадлежит этот конкретный пользователь (например, по кукам), и отправляем его на соответствующую страницу.

- блокировка ботов: по заголовку request.headers['user-agent'] определяем, является ли пользователь ботом.

#nextjs #ссылки
👍3
Как настроить 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
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