Не потрапляйте в пастку мислення, бібліотека повинна прописати, як все робити . Якщо ви хочете зробити щось із тайм-аутом у JavaScript, вам потрібно скористатися setTimeout
. Немає жодної причини, чому дії Redux повинні бути різними.
Redux дійсно пропонує альтернативні способи боротьби з асинхронними речами, але ви повинні використовувати тільки ті , коли ви розумієте , що ви повторюєте занадто багато коди. Якщо у вас немає цієї проблеми, використовуйте те, що пропонує мова, і перейдіть до найпростішого рішення.
Написання асинхронного коду Inline
Це, безумовно, найпростіший спосіб. І тут немає нічого конкретного для Redux.
store.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
store.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)
Аналогічно, зсередини підключеного компонента:
this.props.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
this.props.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)
Єдина відмінність полягає в тому, що у підключеному компоненті ви зазвичай не маєте доступу до самого магазину, але отримуєте або створені dispatch()
конкретні дії, введені як реквізит. Однак це не має для нас ніякої різниці.
Якщо вам не подобається робити помилки під час відправки одних і тих же дій з різних компонентів, ви можете витягнути творців дій замість диспетчеризації об'єктів дії в рядку:
// actions.js
export function showNotification(text) {
return { type: 'SHOW_NOTIFICATION', text }
}
export function hideNotification() {
return { type: 'HIDE_NOTIFICATION' }
}
// component.js
import { showNotification, hideNotification } from '../actions'
this.props.dispatch(showNotification('You just logged in.'))
setTimeout(() => {
this.props.dispatch(hideNotification())
}, 5000)
Або, якщо ви попередньо пов'язали їх із connect()
:
this.props.showNotification('You just logged in.')
setTimeout(() => {
this.props.hideNotification()
}, 5000)
Поки ми не використовували жодного проміжного програмного забезпечення чи іншої передової концепції.
Витяг творця Async Action
Вищенаведений підхід працює добре у простих випадках, але ви можете виявити, що у нього є кілька проблем:
- Це змушує вас дублювати цю логіку в будь-якому місці, де ви хочете показати сповіщення.
- У сповіщеннях немає ідентифікаторів, тому у вас буде стан перегонів, якщо ви покажете два сповіщення досить швидко. Коли перший тайм-аут закінчиться, він буде відправлений
HIDE_NOTIFICATION
, помилково приховавши друге повідомлення раніше, ніж після таймауту.
Щоб вирішити ці проблеми, вам потрібно буде витягти функцію, яка централізує логіку очікування та відправляє ці дві дії. Це може виглядати приблизно так:
// actions.js
function showNotification(id, text) {
return { type: 'SHOW_NOTIFICATION', id, text }
}
function hideNotification(id) {
return { type: 'HIDE_NOTIFICATION', id }
}
let nextNotificationId = 0
export function showNotificationWithTimeout(dispatch, text) {
// Assigning IDs to notifications lets reducer ignore HIDE_NOTIFICATION
// for the notification that is not currently visible.
// Alternatively, we could store the timeout ID and call
// clearTimeout(), but we’d still want to do it in a single place.
const id = nextNotificationId++
dispatch(showNotification(id, text))
setTimeout(() => {
dispatch(hideNotification(id))
}, 5000)
}
Тепер компоненти можна використовувати, showNotificationWithTimeout
не дублюючи цю логіку або не маючи перегонових умов з різними сповіщеннями:
// component.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')
// otherComponent.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged out.')
Чому showNotificationWithTimeout()
приймається dispatch
як перший аргумент? Тому що йому потрібно відправляти дії до магазину. Зазвичай компонент має доступ до, dispatch
але оскільки ми хочемо, щоб зовнішня функція взяла на себе контроль над диспетчеризацією, нам потрібно дати йому контроль над диспетчеризацією.
Якщо у вас був експортований одномісний магазин з якогось модуля, ви можете просто імпортувати його та dispatch
безпосередньо на ньому:
// store.js
export default createStore(reducer)
// actions.js
import store from './store'
// ...
let nextNotificationId = 0
export function showNotificationWithTimeout(text) {
const id = nextNotificationId++
store.dispatch(showNotification(id, text))
setTimeout(() => {
store.dispatch(hideNotification(id))
}, 5000)
}
// component.js
showNotificationWithTimeout('You just logged in.')
// otherComponent.js
showNotificationWithTimeout('You just logged out.')
Це виглядає простіше, але ми не рекомендуємо такий підхід . Основна причина, яка нам не подобається, це те, що вона змушує магазину бути одиноким . Це дуже важко реалізувати серверну візуалізацію . На сервері ви хочете, щоб кожен запит мав власний магазин, щоб різні користувачі отримували різні попередньо завантажені дані.
Магазин, що займається одиночкою, також ускладнює тестування. Ви більше не можете знущатися над магазином під час тестування творців дій, оскільки вони посилаються на конкретний реальний магазин, експортований з певного модуля. Ви навіть не можете скинути його стан ззовні.
Тож, поки ви технічно можете експортувати одномодовий магазин з модуля, ми відмовляємо його. Не робіть цього, якщо ви не впевнені, що ваша програма ніколи не додасть серверну візуалізацію.
Повернення до попередньої версії:
// actions.js
// ...
let nextNotificationId = 0
export function showNotificationWithTimeout(dispatch, text) {
const id = nextNotificationId++
dispatch(showNotification(id, text))
setTimeout(() => {
dispatch(hideNotification(id))
}, 5000)
}
// component.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')
// otherComponent.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged out.')
Це вирішує проблеми з дублюванням логіки і рятує нас від перегонів.
Посереднє програмне забезпечення
Для простих додатків підходу має бути достатньо. Не хвилюйтесь про проміжне програмне забезпечення, якщо ви цим задоволені.
Однак у великих програмах ви можете виявити певні незручності навколо нього.
Наприклад, здається прикро, що нам доводиться проїжджати dispatch
. Це робить складнішим розділення контейнерних та презентаційних компонентів, оскільки будь-який компонент, який асинхронно розсилає дії Redux таким способом, повинен приймати dispatch
як опору, щоб він міг передавати його далі. Ви більше не можете зв’язати творців дій, connect()
оскільки showNotificationWithTimeout()
це насправді не творець дії. Він не повертає дії Redux.
Крім того, може бути незручно згадати, які функції подібні творцям синхронних дій, showNotification()
а які - асинхронними помічниками showNotificationWithTimeout()
. Ви повинні використовувати їх по-різному і бути обережними, щоб не помилитися між собою.
Це було мотивацією для пошуку способу «легітимізувати» цю модель надання dispatch
функції помічника і допомогти Redux «бачити» таких творців асинхронних дій як особливий випадок творців звичайних дій, а не зовсім інших функцій.
Якщо ви все ще з нами, і ви також визнали проблемою у вашому додатку, ви можете використовувати проміжне програмне забезпечення Redux Thunk .
Redux Thunk в дусі вчить Redux розпізнавати особливі види дій, які насправді є функціями:
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
const store = createStore(
reducer,
applyMiddleware(thunk)
)
// It still recognizes plain object actions
store.dispatch({ type: 'INCREMENT' })
// But with thunk middleware, it also recognizes functions
store.dispatch(function (dispatch) {
// ... which themselves may dispatch many times
dispatch({ type: 'INCREMENT' })
dispatch({ type: 'INCREMENT' })
dispatch({ type: 'INCREMENT' })
setTimeout(() => {
// ... even asynchronously!
dispatch({ type: 'DECREMENT' })
}, 1000)
})
Якщо це посереднє програмне забезпечення ввімкнено, якщо ви відправляєте функцію , проміжне програмне забезпечення Redux Thunk подасть його dispatch
як аргумент. Це також "проковтне" такі дії, тому не турбуйтеся про те, щоб ваші редуктори отримували дивні аргументи функції. Ваші редуктори будуть отримувати лише дії простого об’єкта - або безпосередньо, або випромінювані функціями, як ми нещодавно описали.
Це виглядає не дуже корисно, чи не так? Не в цій конкретній ситуації. Однак це дозволяє нам заявляти showNotificationWithTimeout()
як про звичайного творця Redux:
// actions.js
function showNotification(id, text) {
return { type: 'SHOW_NOTIFICATION', id, text }
}
function hideNotification(id) {
return { type: 'HIDE_NOTIFICATION', id }
}
let nextNotificationId = 0
export function showNotificationWithTimeout(text) {
return function (dispatch) {
const id = nextNotificationId++
dispatch(showNotification(id, text))
setTimeout(() => {
dispatch(hideNotification(id))
}, 5000)
}
}
Зверніть увагу, наскільки функція майже ідентична тій, яку ми писали в попередньому розділі. Однак це не сприймається dispatch
як перший аргумент. Натомість він повертає функцію, яка приймає dispatch
як перший аргумент.
Як би ми використовували його в нашому компоненті? Однозначно, ми могли б написати це:
// component.js
showNotificationWithTimeout('You just logged in.')(this.props.dispatch)
Ми закликаємо творця дій асинхронізації, щоб отримати внутрішню функцію, яка хоче просто dispatch
, і тоді ми передаємо dispatch
.
Однак це навіть більш незручно, ніж оригінальна версія! Чому ми навіть пішли таким шляхом?
Через те, що я вам казав раніше. Якщо середнє програмне забезпечення Redux Thunk увімкнено, щоразу, коли ви намагаєтесь передати функцію замість об’єкта дії, проміжне програмне забезпечення буде викликати цю функцію із dispatch
самим методом як перший аргумент .
Тож ми можемо зробити це замість цього:
// component.js
this.props.dispatch(showNotificationWithTimeout('You just logged in.'))
Нарешті, диспетчеризація асинхронної дії (насправді, серія дій) виглядає не інакше, ніж диспетчеризація однієї дії синхронно з компонентом. Що добре, тому що компонентам не важливо, чи щось відбувається синхронно чи асинхронно. Ми просто це абстрагували.
Зауважте, що оскільки ми «навчили» Redux розпізнавати таких «особливих» творців дій (ми їх називаємо тханими творцями дій), тепер ми можемо використовувати їх у будь-якому місці, де ми б використовували звичайних творців дій. Наприклад, ми можемо використовувати їх із connect()
:
// actions.js
function showNotification(id, text) {
return { type: 'SHOW_NOTIFICATION', id, text }
}
function hideNotification(id) {
return { type: 'HIDE_NOTIFICATION', id }
}
let nextNotificationId = 0
export function showNotificationWithTimeout(text) {
return function (dispatch) {
const id = nextNotificationId++
dispatch(showNotification(id, text))
setTimeout(() => {
dispatch(hideNotification(id))
}, 5000)
}
}
// component.js
import { connect } from 'react-redux'
// ...
this.props.showNotificationWithTimeout('You just logged in.')
// ...
export default connect(
mapStateToProps,
{ showNotificationWithTimeout }
)(MyComponent)
Читаючий штат
Зазвичай ваші редуктори містять бізнес-логіку для визначення наступного стану. Однак редуктори запускаються тільки після відправлення дій. Що робити, якщо у творця грудної дії є побічний ефект (наприклад, виклик API), і ви хочете запобігти його за певної умови
Не використовуючи програмне забезпечення Thunk, ви просто зробите цю перевірку всередині компонента:
// component.js
if (this.props.areNotificationsEnabled) {
showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')
}
Однак метою вилучення творця дії було централізація цієї повторюваної логіки на багатьох компонентах. На щастя, Redux Thunk пропонує вам спосіб прочитати поточний стан магазину Redux. Окрім цього dispatch
, він також передається getState
як другий аргумент функції, яку ви повернетесь від свого творця грубої дії. Це дозволяє грунтовці прочитати поточний стан магазину.
let nextNotificationId = 0
export function showNotificationWithTimeout(text) {
return function (dispatch, getState) {
// Unlike in a regular action creator, we can exit early in a thunk
// Redux doesn’t care about its return value (or lack of it)
if (!getState().areNotificationsEnabled) {
return
}
const id = nextNotificationId++
dispatch(showNotification(id, text))
setTimeout(() => {
dispatch(hideNotification(id))
}, 5000)
}
}
Не зловживайте цією схемою. Це добре для отримання викликів API, коли є кешовані дані, але це не дуже хороший фундамент для побудови вашої бізнес-логіки. Якщо ви використовуєте getState()
лише умовне відправлення різних дій, замість цього введіть бізнес-логіку в редуктори.
Наступні кроки
Тепер, коли ви маєте основну інтуїцію щодо того, як працюють громозахисні матеріали, ознайомтеся з прикладом Redux async, який їх використовує.
Ви можете знайти багато прикладів, в яких грози повертають обіцянки. Це не потрібно, але може бути дуже зручно. Redux не хвилює те, що ти повертаєшся з грудної клітки, але він дає тобі повернене значення dispatch()
. Ось чому ви можете повернути Обіцянку від грому і дочекатися його виконання, зателефонувавши dispatch(someThunkReturningPromise()).then(...)
.
Ви також можете розділити складних творців загрозливих дій на декілька менших творців загрозливих дій. dispatch
Метод , що надається санках може прийняти санки самого по собі, так що ви можете застосувати шаблон рекурсивно. Знову ж таки, це найкраще працює з Обіцянками, оскільки поверх цього ви можете реалізувати асинхронний потік управління.
У деяких додатках ви можете опинитися в ситуації, коли ваші вимоги до асинхронного потоку управління є надто складними, щоб їх можна було виразити громами. Наприклад, повторна спроба невдалих запитів, потік повторної авторизації за допомогою лексем або покрокове включення може бути занадто багатослівним і помилковим, коли написано таким чином. У цьому випадку ви можете поглянути на більш досконалі рішення для асинхронного потоку управління, такі як Redux Saga або Redux Loop . Оцініть їх, порівняйте приклади, відповідні вашим потребам, і виберіть той, який вам найбільше подобається.
Нарешті, не використовуйте нічого (включаючи громовідводи), якщо у вас немає справжньої потреби в них. Пам'ятайте, що залежно від вимог ваше рішення може виглядати так само просто
store.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
store.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)
Не потійте, якщо ви не знаєте, чому це робите.
redux-saga
відповідь, якщо ви хочете чогось кращого, ніж громи. Пізня відповідь, тому вам доведеться прокручувати довгий час, перш ніж з'явитись на екрані :), це не означає, що читати не варто. Ось ярлик: stackoverflow.com/a/38574266/82609