thunk react что это
Разбираемся в redux-saga: От генераторов действий к сагам
Любой redux разработчик расскажет вам, что одной из самых тяжелейших частей разработки приложений являются асинхронные вызовы — как вы будете обрабатывать реквесты, таймауты и другие коллбэки без усложнения redux действий(actions) и редьюсеров(reducers).
В этой статье я опишу несколько различных подходов к управлению асинхронностью в вашем приложении, начиная от простых подходов как redux-thunk, заканчивая более продвинутыми библиотеками вроде redux-saga.
Мы собираемся использовать React и Redux, поэтому будем полагать, что вы имеете хотя бы какое то представление о том как они работают.
Генераторы действий (Action creators)
Взаимодействие с API довольно частое требование в приложениях. Представьте, что нам необходимо показывать случайную картинку собаки, когда мы нажимаем на кнопку.
мы можем использовать Dog CEO API и что-то довольно простое вроде вызова fetch внутри генератора действия (action creator).
Нет ничего плохого в таком подходе. При прочих равных всегда лучше использовать более простой подход.
Однако, использование только Redux не дает нам достаточно гибкости. Ядро Redux это контейнер состояния (state container), который поддерживает только синхронные потоки данных.
На каждое действие, в хранилище (store) посылается объект, описывающий что произошло, затем вызывается редюсер (reducer) и состояние (state) сразу обновляется.
Но в случае асинхронного вызова, вам необходимо сначала дождаться ответа и затем уже, если не было ошибок, обновить состояние. А что если у вашего приложения есть некая сложная логика/workflow?
Для этого Redux использует промежуточные слои (middlewares). Промежуточный слой это кусок кода, который выполняется после отправки действия, но перед вызовом редюсера.
Промежуточные слои могут соединяться в цепочку вызовов для различной обработки действия (action), но на выходе обязательно должен быть простой объект (действие)
Для асинхронных операций, Redux предлагает использовать redux-thunk промежуточный слой.
Redux-thunk
Redux-thunk является стандартным путем выполнения асинхронных операций в Redux.
Для нашей цели, redux-thunk вводит понятие преобразователь(thunk), что является функцией, которая предоставляет отложенное выполнение, по необходимости.
Значение 3 сразу присваивается переменной x.
Однако, если у нас есть выражение наподобие
То суммирование выполняется не сразу, а только при вызове функции foo(). Это делает функцию foo преобразователем(thunk).
Redux-thunk позволяет генератору действия (action creator) отправлять функцию в дополнении к объекту, конвертируя таким образом генератор действия в преобразователь.
Ниже, мы перепишем предыдущий пример используя redux-thunk
На первый взгляд он не сильно отличается от предыдущей версии.
Преимуществом использования redux-thunk является то, что компонент не знает, что выполняется асинхронное действие.
Т.к. промежуточный слой автоматически передает функцию dispatch в функцию, которую возвращает генератор действий, то снаружи, для компонента, нет никакой разницы в вызове синхронных и асинхронных действий (и компонентам больше не нужно об этом беспокоиться)
Таким образом, с помощью механизма промежуточных слоев, мы добавили неявный слой (a layer of indirection), который дал нам больше гибкости.
Поскольку redux-thunk передает в возвращаемые функции методы dispatch и getState из хранилища (store) как параметры, то вы можете отсылать другие действия и использовать состояние (state) для реализации дополнительной логики и workflow.
Но что если у нас есть что-то более сложное, чтобы быть выраженным с помощью преобразователя (thunk), без изменения react компонента. В этом случае мы можем попробовать использовать другую библиотеку промежуточных слоев (middleware library) и получить больше контроля.
Давайте посмотрим как заменить redux-thunk на библиотеку, что может дать нам больше контроля — redux-saga.
Redux-saga
Redux-saga это библиотека нацеленная делать сайд-эффекты проще и лучше путем работы с сагами.
Саги это дизайн паттерн, который пришел из мира распределенных транзакций, где сага управляет процессами, которые необходимо выполнять транзакционным способом, сохраняя состояние выполнения и компенсируя неудачные процессы.
Чтобы узнать больше о сагах можно начать с просмотра Применения паттерна Сага от Caitie McCaffrey, ну а если вы амбициозны, то здесь Статья, которая первая описывает саги в отношении распределенных систем.
В контексте Redux, сага реализована как промежуточный слой (мы не можем использовать редюсеры потому что они должны быть чистыми функциями), который координирует и побуждает асинхронные действия (сайд-эффекты).
Redux-saga делает это с помощью ES6 генераторов
Генераторы (Generators) это функции которые могут быть остановлены и продолжены, вместо выполнения всех выражений в один проход.
Когда вы вызываете функцию-генератор, она возвращает объект-итератор. И с каждым вызовом метода итератора next() тело функции-генератора будет выполняться до следующего yield выражения и затем останавливаться.
Это делает асинхронный код проще для написания и понимания.
Для примера вместо следующего выражения:
С генераторами мы бы написали так:
Возвращаясь к redux-saga, если говорить в общем, мы имеем сагу чья работа это следить за отправленными действиями (dispatched actions).
Для координирования логики, которую мы хотим реализовать внутри саги, мы можем использовать вспомогательную функцию takeEvery для создания новой саги для выполнения операции.
Если есть несколько запросов, takeEvery стартует несколько экземпляров саги-рабочего (worker saga). Иными словами реализует конкурентность(concurrency) для вас.
Надо отметить, что сага-наблюдатель (watcher saga) является другим неявным слоем (layer of indirection), который дает больше гибкости для реализации сложной логики (но это может быть лишним для простых приложений).
Теперь мы можем реализовать fetchDogAsync() функцию (мы полагаем, что у нас есть доступ к методу dispatch)
Но redux-saga позволяет нам получить объект, который декларирует наше намерение произвести операцию, вместо результата выполнения самой операции. Иными словами, пример выше реализуется в redux-saga следующим образом:
(Прим. переводчика: автор забыл заменить самый первый вызов dispatch)
Вместо вызова асинхронного реквеста напрямую, метод call вернет только объект описывающий эту операцию и redux-saga сможет позаботиться о вызове и возвращении результатов в функцию-генератор.
Тоже самое касается и метода put. Вместо отправления действий (dispatch action) внутри функции-генератора, put возвращает объект с инструкциями для промежуточного слоя (middleware) — отправить действие.
Эти возвращаемые объекты называются Эффекты (Effects). Ниже пример эффекта возвращаемого методом call:
Работая с Эффектами, redux-saga делает саги скорее Декларативными, чем Императивными.
Декларативное программирование это стиль программирования, который пытается минимизировать или устранить сайд-эффекты, описанием что программа должна делать, вместо описания как она должна это делать.
Преимущество, которое это дает, и о чем говорят большинство людей, то что функцию, которая возвращает простой объект, гораздо проще тестировать, чем функцию, которая делает асинхронный вызов. Для тестирования, вам не нужно использовать реальное АПИ, делать фейки или мокать.
Для тестирования, вы просто итерируете функцию-генератор делая assert и сравниваете полученные значения.
Еще одно дополнительное преимущество это возможность легко объединять разные эффекты в сложный workflow.
Возвращаясь к нашему простому примеру, ниже полная реализация в redux-saga:
Когда вы нажимаете на кнопку, вот что происходит:
1. Отправляется действие FETCHED_DOG
2. Сага-наблюдатель (watcher saga) watchFetchDog получает это действие и вызывает сагу-рабочего (worker saga) fetchDogAsync.
3. Отправляется действие по отображению индикатора загрузки.
4. Происходит вызов API метода.
5. Отправляется действие по обновлению состояния (успех или провал)
Если вы считаете, что несколько неявных слоев и чуть-чуть дополнительной работы стоят этого, то redux-saga может дать вам больше контроля для обработки сайд-эффектов функциональным способом.
Заключение
Эта статья показала как реализовать асинхронные операции в Redux с помощью генераторов действий (action creators), преобразователей (thunks), и саг (sagas), идя от простого подхода к более сложному.
Redux не предписывает решение для обработки сайд-эффектов. Когда вы будете решать какому подходу следовать, вам необходимо учитывать сложность вашего приложения. Моя рекомендация — начинать с простого решения.
Также есть альтернативы redux-saga, которые стоит попробовать. Две самых популярных это redux-observable (который базируется на RxJS) и redux-logic (также базирующийся на RxJS наблюдателях, но дающий свободу писать вашу логику в других стилях).
Redux и Thunk вместе с React. Руководство для чайников.
Если вы также как и я читали документацию к Redux, смотрели видео от Dan’а, прошли курс от Wes’а и до сих пор не совсем понимаете как использовать Redux, то я надеюсь что эта статья поможет вам.
После нескольких попыток использования Redux я начал что-то понимать и подумал, что лучше записать процесс преобразования уже существующего приложения в приложение использующее Redux и Redux Thunk. Если вы не знаете что такое Thunk, не беспокойтесь, мы будем использовать его для отправки асинхронных запросов с Redux.
Это руководство предполагает, что у вас есть базовые знания React и ES6/2015.
Способ без Redux
Создание основы
Не так уж и много, но это хорошее начало.
Делаем его динамичным
Жёстко закодированные элементы не сделают ничего хорошего для нашего компонента, поэтому давайте получим наш массив items через JSON API, который также позволит нам устанавливать isLoading и hasErrored в зависимости от обстоятельств.
В нашем случае ответ API будет идентичен тому что мы жёстко закодировали в предыдущем примере, но в реальном приложении вы можете получать например список книг, новые записи в блоге, короче то что нужно вашему приложению.
Чтобы получить элементы мы воспользуемся такой штукой, которая называется Fetch API. Fetch позволяет выполнять запросы намного проще чем классический XMLHttpRequest и возвращает промис (который важен для Thunk). Fetch доступен не для всех браузеров, поэтому нам нужно добавить его в зависимости нашего проекта:
Преобразования на самом деле очень простые.
Вот что у нас получится в итоге (не изменённые строки опущены):
Но на самом деле, компонент не должен содержать в себе логику связанную с получением данных и сами данные не должны храниться в состоянии компонента. Вот здесь то и приходит Redux.
Преобразование в Redux
Есть несколько основных принципов в Redux, которые нужно понимать:
, который оборачивает всё наше приложение и передаёт хранилище store всем дочерним элементам.
Всё это должно стать более понятным, когда мы начнем преобразовывать наше приложение в Redux.
Проектирование нашего состояния
Создание наших действий
Два первых создателя действий принимают логическое значение bool (true/false) в качестве аргумента и возвращают объект содержащий type и bool в соответствующих свойствах.
Обратите внимание на то, что значение которое вы используете для свойства type и имя другого свойства возвращаемого объекта очень важны, потому что мы будем использовать их снова в редьюсерах.
Теперь когда у нас есть 3 действия, которые будут представлять наше состояние, мы преобразуем оригинальный метод fetchData() нашего компонента в метод itemsFetchData() нашего создателя действия.
По настоящему простым примером может быть отправка действия itemsHasErrored() через пять секунд.
Создание редьюсеров
С действиями определились, теперь напишем редьюсеры, которые будут брать эти действия и возвращать новое состояние приложения.
Примечание: В Redux всегда вызываются все редьюсеры независимо от того какое действие было отправлено, поэтому внутри каждого из них нужно возвращать оригинал состояния, если не действие не применяется.
Каждый редьюсер принимает два параметра: предыдущее состояние ( state ) и объект действия ( action ).
Если action.type совпадает с текущим действием, то мы возвращаем подходящее свойство этого действия. Как уже говорилось ранее, action.type и action[propertyName] это то, что мы определяли в создателях действий.
Отличается он потому что у него может быть несколько условий, например он может возвращать массив со всеми элементами, а может возвращать какое-то подмножество после того как было отправлено действие «удаления», а может вообще возвращать пустой массив, если были удалены все элементы.
Каждый редьюсер будет возвращать отдельное свойство состояния, не зависимо от того сколько условий в этом редьюсере. Мне нужно было некоторое время, чтобы это понять.
Настройка хранилища и предоставление его приложению
Это довольно просто. Давайте создадим файл store/configureStore.js со следующим содержимым:
Теперь изменим файл index.js нашего приложения и добавим в него
Чтобы добраться до этой стадии нужно приложить совсем немного усилий, и теперь когда мы закончили с настройкой, можно изменить наш компонент чтобы использовать всё то, что уже сделали.
Преобразование компонента для использования Redux хранилища и методов
В верхней части файла нужно изменить секцию import :
После определения класса нашего компонента нам нужно сопоставить состояние Redux и отправку нашего создателя действия свойствам компонента.
И нам нужна ещё одна функция, чтобы иметь возможность отправлять наш создатель действий itemsFetchData() со свойством.
Пока что эти 2 функции ничего не делают, так как нам нужно изменить завершающую строку экспорта ( export ).
Эта строка соединяет наш ItemList с Redux, чтобы во время отображение мы могли использовать свойства.
Последний шаг заключается в преобразовании нашего компонента для использования свойств вместо состояния, и удаление ненужных остатков.
Ваш компонент должен выглядеть следующим образом:
Вот и всё. Теперь приложение использует Redux и Redux Thunk для получения и отображения данных.
Это было не так сложно, не так ли?
Теперь вы Redux мастер 😀
Что дальше?
Весь код данного урока я выложил на Github, с коммитами для каждого шага. Я хочу чтобы вы клонировали его, запустили и поняли как он работает, а после этого добавили возможность удаления определённых элементов из списка, основываясь на позициях элемента.
Введение в React и Redux для бекенд-разработчиков
Если вы как я долгое время считали, что JavaScript – это такой «игрушечный» язык на котором пишут анимашки для менюшек и падающие снежинки на форумах под новый год, а потом очнулись в 2016 году с мыслями WTF: react, flux redux, webpack, babel,… не отчаивайтесь. Вы не одиноки. Материалов по современному фронтенду в сети много, даже слишком много. Под катом еще одно альтернативное мнение о том, каково это учить JavaScript в 2016 году.
Итак, нам потребуются: React, React Dev Tools, React-Hot-Loader, React-Router, Thunk, Redux, Redux Dev Tools, Semantic-UI, Webpack, Babel и npm.
На первый взгляд много. Сравним с бекендом: MVC-фреймворк, ORM, Data Mapper, IOC-контейнер, логер, профайлер, очереди, управление конфигурациями, сборка и выкладка… Список можно продолжить, но думаю идея понятна и так: с ростом сложности решаемых задач растет и сложность инструментов. Все чаще мы употребляем термин Web App вместо Web Site, акцентируя внимание на богатых возможностях современных веб-приложений.
Почему именно этот стек?
Если вдуматься и отбросить все лишнее, то единственный ресурс постиндустриальной эпохи, через который можно выразить все остальные – это время. Освоение каждой новой технологии требует затрат времени, а значит более перспективны инвестиции в технологии, которые не устареют в ближайшие пару месяцев. Дополнительный плюс получают технологии с пологой кривой обучения.
React + Redux VS Angular VS Yet Another JS Framework
Только ленивый не сравнил Angular с React’ом (приписав при этом, дескать, сравнение не корректно, Angular – фреймворк, React – библиотека). Пойдем от обратного. Почему бы не выбрать что-нибудь эдакое, типа Vue, Ember или, упаси боже, Elm?
Если вам нравится TypeScript, ничто не мешает использовать его вместе с React. Просто конкретно мне он пока не дал критического объема преимуществ, чтобы заставить тратить время на еще один элемент в стеке.
Да, Redux можно использовать и с Angular, он никак не привязан к React, но для React уже есть react-redux и react-hot-loader. Наверное, для Angular тоже есть, но мейнтейнер Redux’а явно на стороне React.
Для React и Redux доступно два расширения Chrome. Рекомендую поставить оба, чтобы сделать отладку приятной.
Есть еще всякие ништяки, вроде React Native, но я им не пользовался, поэтому поделиться на эту тему мне, к сожалению, нечем.
А flux и все эти модные словечки. Как это работает?
Пакет react-redux осуществляет односторонний байндинг redux state => react component с помощью метода connect. При изменении состояния Redux сам перерисует необходимые компоненты, передав в props dispatch и часть общего state, хранимого в Redux. Какую часть состояния и какие функции на основании store.dispatch передавать — решать вам. Я рекомендую передать все обработчики событий компонента и не «светить» dispatch в компонентах.
State содержится в Redux, но и у компонентов есть свой state. Какой из них использовать?
Разработчик Redux предлагает делать как удобнее. Это не совсем формальный совет. У нас сложилась практика использовать state компонента только для форм или в целях оптимизации.
JSX – это не JavaScript. Да, React можно писать без JSX, но проще тогда без React’а. Вообще ситуация с HTML-шаблонами напоминает мне засилье шаблонизаторов для PHP лет десять назад. Самым монструозным из всех был конечно Smarty. Мне казалось, что люди сошли с ума. Как иначе можно было объяснить желание написать шаблонизатор… для шаблонизатора?
Функциональная запись короче и лучше читается. Но я рекомендую не увлекаться экономией строк и вместо:
использовать чуть менее лаконичное, но более безопасное:
Во-первых, если вы не передадите параметры, то React недвусмысленно намекнет в консоли, что вы не правы. Во-вторых, WebStorm умеет анализировать PropTypes и при авто-дополнении заботливо вставит все required props.
Babel
Если вы не поняли на каком языке примеры кода выше, не расстраивайтесь. Это не JavaScript, ну не совсем JavaScript. Это ES6 + JSX. С JSX мы разобрались в параграфе выше – это просто синтаксический сахар для шаблонизации (почти как в PHP или @Model.Param в Razor).
С ES6 ситуация чуть сложнее и запутаннее. Если коротко:
Стоит отметить, что ES6 – это не истина в последней инстанции. Некоторые фичи до сих пор являются экспериментальными (например, генераторы) и для их использования потребуются полифиллы (библиотеки, реализующие экспериментальные фичи стандарта). Частично из-за этого мы решили отказаться от redux-saga в пользу redux-thunk, хотя и идея диспатчить функции до сих пор не кажется мне изящной (она просто работает).
Webpack
Так, то есть пишем мы на ES6 + JSX, а в бразуере выполняется минифицированный JS. Все это напоминает историю изобретения высокоуровневых ЯП. Люди могли писать более эффективные программы на ассемблере, но предпочли удобство и продуктивность. Раз есть исходники и компилятор (транспилятор в нашем случае), то потребуется и система сборки. Если в вашей пещере было достаточно тепло и уютно, возможно, названия grunt и gulp вам ничего не говорят. Что к лучшему. На данный момент, можно считать (слава богу), что для JS есть только один сборщик – Webpack — оставивший конкурентов позади. Можно считать, что webpack — аналог maven или msbuild (кому что ближе) в мире фронтэнда. Не смотря на то что, конфиги webpack’а на первый взгляд напоминают некромантские свитки, через какое-то время привыкаешь. Наверное, каждый любитель фронтенда должен хотя-бы раз в своей жизни написать tutorial по настройке webpack, также как каждый фанат ФП – tutorial по монадам.
Вообще Webpack собирает не только JS, но еще и sass, svg, шрифты и вообще все что душе угодно, но я пока еще не готов написать полноценный туториал, так что поищите на просторах интернета.
Без этих строк npm run build и npm start не заработают.
React-Hot-Loader
React-Router и Thunk
Основная ниша React’а в Web – это конечно SPA-сайты. А какой SPA-сайт без навигации и общения с сервером. Первую задачу решает react-router. Здесь альтернатив нет. Из неприятных сюрпризов:
Semantic-UI
Раз мы заговорили про SPA-приложения, то кроме навигации и запросов к серверу нужны еще компоненты, которые будут ту самую серверную информацию отображать. Для React есть обвязки Bootstrap, Material UI, Syncfusion Web Essentials (хотя эти обвязки не честные – там внутри jQuery). Наш выбор остановился на Sematic-UI. Решение удалось принять очень быстро – сначала отмели платные компоненты. Material UI не стали использовать из-за обилия анимации (сложнее модифицировать). Остались Bootstrap и Semantic. На Бутстрапе уже пол интернета сделано и в целом, Семантик показался более визуально-привлекательным. В общем, остановились на нем. Сразу оговорюсь, что использование Semantic UI – строго опционально, потому что минифицированная версия весит около 500кб.
Так что разрабатывать фронтенд в 2016 году вполне себе комфортно. Да инструментов много, многие библиотеки не совместимы, новые версии выходят очень часто. Это разумная плата за гигантский скачок в качестве фронтенд-стека.
Зачем нужен redux-thunk?
Сегодня с коллегой возникла небольшая дискуссия, я утверждал, что redux-thunk очень удобная надстройка (middleware) над redux и позволяет создавать асинхронные экшены.
Коллега возразил, зачем нужен redux-thunk, когда можно сделать вот так:
Т.е он импортит store в определенный файл проекта и вызывает несколько экшенов
По началу я впал в ступор, действительно, зачем redux-thunk, если можно испортить стор, но позже проанализировав ситуацию, пришел к выводу, что асинхронные экшены, например как в примере выше загрузка профиля должна иметь 3 состояния:
— старт загрузки профиля (для отображения лоадера)
— данные профиля успешно загружены
— возникла ошибка при загрузке данных
исходя из этого, если действовать по методу коллеги, необходимо создавать для каждого действия отдельную функцию (экшены)
Вот пример с использованием redux-thunk
Преимущества redux-thunk очевидны, и больше всего мне нравится то, что с помощью redux-thunk всю логику можно хранить в экшенах и тем самым разгрузить компоненты, это одновременно делает проект чище, ведь логика будет находится внутри экшенов, вместо того, чтобы импортить стор в разные части проекта
Что вы скажите, какое мнение верно, и имеет ли права на жизнь предложенное коллегой решение?
Если я правильно понял вашего коллегу, то речь была не о том, что нужно асинхронные функции через setTimeout вызывать, а это был просто пример, как сделать какой-то кусочек «типа асинхронным».
Его довод был в том, что вместо вызова dispatch из замыкания (полученного с помощью redux-thunk, например) он вызывал store.dispatch напрямую (то есть у объекта store, и этот store импортировал бы в каждом файле).
В остальном, все очень четко расписано в ответе Дэна Абрамова, который привел holymotion.
Если быть кратким, то вам нужна функция dispatch, ведь именно через нее вы «диспатчите» свои экшены. Вы не можете в асинхронном ответе написать:
Поэтому вы были бы обязаны в каждый action creator, который является асинхронным, передавать бы помимо нужных вам аргументов, еще и функцию dispatch из своего контейнера. Что не удобно. (это все есть по ссылке на stackoverflow, но более подробно)