Як відправити дію Redux з таймаутом?


891

У мене є дія, яка оновлює стан сповіщення моєї заявки. Зазвичай це сповіщення буде помилкою чи інформацією. Потім мені потрібно відправити ще одну дію через 5 секунд, яка поверне стан сповіщення до початкового, тому ніякого повідомлення. Основна причина цього - надання функціональності, коли сповіщення автоматично зникає через 5 секунд.

Мені не пощастило з використанням setTimeoutта поверненням іншої дії і не можу знайти, як це робиться в Інтернеті. Тож будь-яка порада вітається.


30
Не забудьте перевірити мою redux-sagaвідповідь, якщо ви хочете чогось кращого, ніж громи. Пізня відповідь, тому вам доведеться прокручувати довгий час, перш ніж з'явитись на екрані :), це не означає, що читати не варто. Ось ярлик: stackoverflow.com/a/38574266/82609
Себастьян Лорбер

5
Щоразу, коли ви встановлюєте setTimeout, не забудьте очистити таймер, використовуючи clearTimeout в методі життєвого циклу компонента
WillUnMount

2
redux-saga - це круто, але, схоже, вони не підтримують набрані відповіді від функцій генератора. Може мати значення, якщо ви використовуєте машинопис із реагуванням.
Крістіан Рамірес

Відповіді:


2616

Не потрапляйте в пастку мислення, бібліотека повинна прописати, як все робити . Якщо ви хочете зробити щось із тайм-аутом у 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)

Не потійте, якщо ви не знаєте, чому це робите.


27
Асинхронні дії здаються таким простим і елегантним рішенням поширеної проблеми. Чому підтримка для них не використовується для скорочення без необхідності проміжного програмного забезпечення? Тоді ця відповідь могла бути набагато стислішою.
Філ Мандер

83
@PhilMander Оскільки існує багато альтернативних моделей, таких як github.com/raisemarketplace/redux-loop або github.com/yelouafi/redux-saga, які настільки ж витончені (якщо не більше). Redux - це інструмент низького рівня. Ви можете створити суперсет, який вам подобається, і розподілити його окремо.
Дан Абрамов

16
Чи можете ви пояснити це: * розглянути можливість введення ділової логіки в редуктори *, це означає, що я повинен відправити дію, а потім визначити в редукторі, які подальші дії для відправки залежно від мого стану? Моє запитання: чи я потім відправляю інші дії безпосередньо в моєму редукторі, і якщо ні, то звідки я їх відправляю?
froginvasion

25
Це речення стосується лише синхронного відмінка. Наприклад, якщо ви пишете, if (cond) dispatch({ type: 'A' }) else dispatch({ type: 'B' })можливо, вам слід просто dispatch({ type: 'C', something: cond })і вирішити ігнорувати дії в редукторах, а не залежно від action.somethingпоточного стану.
Дан Абрамов

29
@DanAbramov Ви отримали мою нагороду саме за це "Якщо у вас немає цієї проблеми, використовуйте те, що пропонує мова, і шукайте найпростішого рішення". Лише після того, як я зрозумів, хто це написав!
Метт Лейсі

189

Використання Redux-saga

Як сказав Дан Абрамов, якщо ви хочете вдосконалити контроль над своїм асинхронним кодом, ви можете поглянути на редукс-сагу .

Ця відповідь - простий приклад, якщо ви хочете краще пояснити, чому редукс-сага може бути корисною для вашої програми, перевірте цю іншу відповідь .

Загальна ідея полягає в тому, що Redux-saga пропонує інтерпретатор генераторів ES6, який дозволяє вам легко писати асинхронний код, схожий на синхронний код (саме тому ви часто зустрічаєте нескінченну кількість циклів у Redux-saga). Так чи інакше, Redux-saga будує свою власну мову безпосередньо всередині Javascript. Redux-saga спочатку може відчувати себе трохи важко, тому що вам потрібно зрозуміти генератори, а також зрозуміти мову, яку пропонує Redux-saga.

Я спробую тут описати систему сповіщень, яку я побудував на основі редукс-саги. Цей приклад зараз працює у виробництві.

Розширена специфікація системи сповіщень

  • Ви можете попросити відображення повідомлення
  • Ви можете надіслати запит на приховування
  • Повідомлення не повинно відображатися більше 4 секунд
  • Одночасно можуть відображатися кілька повідомлень
  • Одночасно може відображатися не більше 3 повідомлень
  • Якщо сповіщення запитується, поки у вас є вже 3 відображених сповіщення, то чергуйте / відкладіть його.

Результат

Знімок екрана мого виробничого додатка Stample.co

тости

Код

Тут я назвав повідомлення сповіщенням, toastале це іменна інформація.

function* toastSaga() {

    // Some config constants
    const MaxToasts = 3;
    const ToastDisplayTime = 4000;


    // Local generator state: you can put this state in Redux store
    // if it's really important to you, in my case it's not really
    let pendingToasts = []; // A queue of toasts waiting to be displayed
    let activeToasts = []; // Toasts currently displayed


    // Trigger the display of a toast for 4 seconds
    function* displayToast(toast) {
        if ( activeToasts.length >= MaxToasts ) {
            throw new Error("can't display more than " + MaxToasts + " at the same time");
        }
        activeToasts = [...activeToasts,toast]; // Add to active toasts
        yield put(events.toastDisplayed(toast)); // Display the toast (put means dispatch)
        yield call(delay,ToastDisplayTime); // Wait 4 seconds
        yield put(events.toastHidden(toast)); // Hide the toast
        activeToasts = _.without(activeToasts,toast); // Remove from active toasts
    }

    // Everytime we receive a toast display request, we put that request in the queue
    function* toastRequestsWatcher() {
        while ( true ) {
            // Take means the saga will block until TOAST_DISPLAY_REQUESTED action is dispatched
            const event = yield take(Names.TOAST_DISPLAY_REQUESTED);
            const newToast = event.data.toastData;
            pendingToasts = [...pendingToasts,newToast];
        }
    }


    // We try to read the queued toasts periodically and display a toast if it's a good time to do so...
    function* toastScheduler() {
        while ( true ) {
            const canDisplayToast = activeToasts.length < MaxToasts && pendingToasts.length > 0;
            if ( canDisplayToast ) {
                // We display the first pending toast of the queue
                const [firstToast,...remainingToasts] = pendingToasts;
                pendingToasts = remainingToasts;
                // Fork means we are creating a subprocess that will handle the display of a single toast
                yield fork(displayToast,firstToast);
                // Add little delay so that 2 concurrent toast requests aren't display at the same time
                yield call(delay,300);
            }
            else {
                yield call(delay,50);
            }
        }
    }

    // This toast saga is a composition of 2 smaller "sub-sagas" (we could also have used fork/spawn effects here, the difference is quite subtile: it depends if you want toastSaga to block)
    yield [
        call(toastRequestsWatcher),
        call(toastScheduler)
    ]
}

І редуктор:

const reducer = (state = [],event) => {
    switch (event.name) {
        case Names.TOAST_DISPLAYED:
            return [...state,event.data.toastData];
        case Names.TOAST_HIDDEN:
            return _.without(state,event.data.toastData);
        default:
            return state;
    }
};

Використання

Ви можете просто відправити TOAST_DISPLAY_REQUESTEDподії. Якщо ви відправляєте 4 запити, відображатимуться лише 3 сповіщення, а 4-е з’явиться трохи пізніше, коли зникне перше повідомлення.

Зауважте, що я не рекомендую спеціально відправляти TOAST_DISPLAY_REQUESTEDз JSX. Ви краще додасте ще одну сагу, яка прослуховує ваші вже наявні події програми, а потім розіслаєTOAST_DISPLAY_REQUESTED : ваш компонент, який запускає сповіщення, не повинен бути щільно пов'язаний із системою сповіщень.

Висновок

Мій код не ідеальний, але працює у виробництві з 0 помилок протягом місяців. Redux-saga та генератори спочатку трохи важкі, але як тільки ви їх зрозумієте, цю систему досить легко побудувати.

Це навіть досить просто реалізувати складніші правила, наприклад:

  • коли занадто багато сповіщень "у черзі", надайте менше часу для кожного повідомлення, щоб розмір черги міг швидше зменшуватися.
  • виявити зміни розміру вікна та відповідно змінити максимальну кількість відображуваних сповіщень (наприклад, робочий стіл = 3, портрет телефону = 2, пейзаж телефону = 1)

Чесно кажучи, удачі, що правильно реалізувати подібні речі за допомогою гронів.

Зауважте, що ви можете робити саме такі речі з редукційним спостереженням, яке дуже схоже на редукс-сагу. Це майже те саме, що викликає смак між генераторами та RxJS.


18
Я хочу, щоб ваша відповідь була раніше, коли запитання було задано, тому що я не можу більше погодитися з використанням бібліотеки побічних ефектів Saga для такої бізнес-логіки. Редуктори та дії створюють переходи стану. Робочі процеси не збігаються з функціями переходу стану. Робочі процеси переходять через переходи, але самі по собі не є переходами. Redux + React бракує цього самостійно - саме тому Redux Saga настільки корисний.
Atticus

4
Дякую, я намагаюся зробити все можливе, щоб з цих причин зробити редукс-сагу популярною :) занадто мало людей думають, що в даний час редук-сага - це лише заміна громовідводам і не бачать, як редук-сага дозволяє складні та роз'єднані робочі процеси
Себастьян Лорбер

1
Саме так. Дії та редуктори - це частина державної машини. Іноді для складних робочих процесів потрібно щось інше, щоб упорядкувати державну машину, яка не є безпосередньо частиною самої державної машини!
Atticus

2
Дії: корисні навантаження / події до стану переходу. Редуктори: функції переходу стану. Компоненти: Інтерфейси користувача, що відображають стан. Але є один головний фрагмент - як ти керуєш процесом багатьох переходів, які мають власну логіку, яка визначає, який перехід робити далі? Redux Saga!
Atticus

2
@mrbrdo, якщо ви уважно прочитаєте мою відповідь, ви помітите, що фактично обробляються тайм-аути сповіщень yield call(delay,timeoutValue);: це не той самий API, але він має той же ефект
Себастьян Лорбер

25

Сховище із зразковими проектами

Наразі є чотири зразкові проекти:

  1. Написання асинхронного коду Inline
  2. Витяг творця Async Action
  3. Використовуйте Redux Thunk
  4. Використовуйте саду Redux Saga

Прийнята відповідь приголомшлива.

Але чогось не вистачає:

  1. Немає зразків, які можна виконати, лише деякі фрагменти коду.
  2. Немає зразкового коду для інших альтернатив, таких як:
    1. Редукс Сага

Тому я створив сховище Hello Async, щоб додати відсутні речі:

  1. Виконані проекти. Ви можете завантажити та запустити їх без змін.
  2. Надайте зразок коду для інших варіантів:

Редукс Сага

У прийнятій відповіді вже наведено приклади фрагментів коду для Async Code Inline, Async Action Generator та Redux Thunk. Для повноти я надаю фрагменти коду для Redux Saga:

// actions.js

export const showNotification = (id, text) => {
  return { type: 'SHOW_NOTIFICATION', id, text }
}

export const hideNotification = (id) => {
  return { type: 'HIDE_NOTIFICATION', id }
}

export const showNotificationWithTimeout = (text) => {
  return { type: 'SHOW_NOTIFICATION_WITH_TIMEOUT', text }
}

Дії прості та чисті.

// component.js

import { connect } from 'react-redux'

// ...

this.props.showNotificationWithTimeout('You just logged in.')

// ...

export default connect(
  mapStateToProps,
  { showNotificationWithTimeout }
)(MyComponent)

Ніщо не є особливим із компонентом.

// sagas.js

import { takeEvery, delay } from 'redux-saga'
import { put } from 'redux-saga/effects'
import { showNotification, hideNotification } from './actions'

// Worker saga
let nextNotificationId = 0
function* showNotificationWithTimeout (action) {
  const id = nextNotificationId++
  yield put(showNotification(id, action.text))
  yield delay(5000)
  yield put(hideNotification(id))
}

// Watcher saga, will invoke worker saga above upon action 'SHOW_NOTIFICATION_WITH_TIMEOUT'
function* notificationSaga () {
  yield takeEvery('SHOW_NOTIFICATION_WITH_TIMEOUT', showNotificationWithTimeout)
}

export default notificationSaga

Саги базуються на генераторах ES6

// index.js

import createSagaMiddleware from 'redux-saga'
import saga from './sagas'

const sagaMiddleware = createSagaMiddleware()

const store = createStore(
  reducer,
  applyMiddleware(sagaMiddleware)
)

sagaMiddleware.run(saga)

У порівнянні з Redux Thunk

Плюси

  • Ви не опинитесь в пеклі.
  • Ви можете легко протестувати свої асинхронні потоки.
  • Ваші дії залишаються чистими.

Мінуси

  • Це залежить від генераторів ES6, який є відносно новим.

Будь ласка, зверніться до проекту, який можна виконати, якщо наведені вище фрагменти коду не відповідають на всі ваші запитання.


23

Зробити це можна за допомогою редук-тунк . Існує посібник з редукційного документа для асинхронних дій, таких як setTimeout.


Просто швидке запитання щодо використання проміжного програмного забезпечення applyMiddleware(ReduxPromise, thunk)(createStore)- це те, як ви додаєте кілька проміжних програм (кома відокремлена?), Оскільки я не можу працювати.
Ілля

1
@Ilja Це повинно спрацювати:const store = createStore(reducer, applyMiddleware([ReduxPromise, thunk]));
geniuscarrier

22

Я б рекомендував також поглянути на схему SAM .

Зразок SAM виступає за включення "предикату наступної дії", де (автоматичні) дії, такі як "сповіщення зникають автоматично через 5 секунд", після того, як модель була оновлена ​​(модель SAM - стан редуктора + зберігання).

Шаблон виступає за послідовність дій та мутацій моделі одна за одною, оскільки "керуючий стан" моделі "контролює", які дії ввімкнено та / або автоматично виконані присудком наступної дії. Ви просто не можете передбачити (загалом), у якому стані система буде переробляти дію, а отже, чи буде дозволена / можлива наступна очікувана дія.

Так, наприклад, код,

export function showNotificationWithTimeout(dispatch, text) {
  const id = nextNotificationId++
  dispatch(showNotification(id, text))

  setTimeout(() => {
    dispatch(hideNotification(id))
  }, 5000)
}

забороняється використовувати SAM, тому що той факт, що може бути відправлена ​​дія приховування, залежить від моделі, яка успішно приймає значення "showNotication: true". Можуть бути й інші частини моделі, які заважають їй прийняти її, і тому не було б ніяких причин запускати дію приховати повідомлення.

Я настійно рекомендую застосувати правильний предикат наступних дій після оновлень магазину і нового стану керування моделі можна знати. Це найбезпечніший спосіб реалізувати поведінку, яку ви шукаєте.

Ви можете приєднатися до нас на Gitter, якщо хочете. Тут також доступний посібник із використання SAM .


Я поки що почухав поверхню, але мене вже вражає модель SAM. V = S( vm( M.present( A(data) ) ), nap(M))просто красиво. Дякуємо, що поділилися своїми думками та досвідом. Я копаю глибше.

@ftor, дякую! коли я написав це вперше, у мене було те саме почуття. Я використовую SAM у виробництві вже майже рік, і я не можу придумати час, коли я відчував, що мені потрібна бібліотека для впровадження SAM (навіть vdom, хоча я бачу, коли це можна було б використовувати). Всього один рядок коду, це все! SAM виробляє ізоморфний код, немає ніякої неоднозначності, де боротися з асинхронними дзвінками ... Я не можу придумати час, коли я хоч, що я роблю?
метапрограміст

SAM - справжня модель програмної інженерії (щойно створила Alexa SDK). Він заснований на TLA + і намагається донести силу цієї неймовірної роботи кожному розробнику. SAM виправляє три наближення, які (в значній мірі) всі використовують десятиліттями: - дії можуть маніпулювати станом програми - призначення прирівнюються до мутації - немає точного визначення того, що таке крок програмування (наприклад, це крок = b * ca , є 1 / прочитати b, c 2 / обчислити b * c, 3 / призначити a з результатом три різні кроки?
метапрограміст

20

Спробувавши різні популярні підходи (творці дій, громозахиси, саги, епоси, ефекти, спеціальні проміжні програми), я все-таки відчув, що, можливо, є місце для вдосконалення, тому я задокументував свою подорож у цій статті блогу, куди я вкладу свою ділову логіку додаток React / Redux?

Як і тут дискусії, я намагався протиставити та порівняти різні підходи. Врешті-решт це призвело до того, що я запровадив нову бібліотеку- редукс-логіку, яка отримує натхнення з епосів, саг, спеціальних програм.

Це дозволяє перехоплювати дії для перевірки, перевірки, авторизації, а також надає спосіб виконання асинхронного вводу.

Деякі поширені функціональні можливості можна просто оголосити, як деблокування, вимкнення, скасування та використання лише відповіді з останнього запиту (takeLatest). Редукс-логіка обгортає ваш код, забезпечуючи цю функціональність для вас.

Це звільняє вас від реалізації вашої основної бізнес-логіки, як би ви не хотіли. Вам не доведеться використовувати спостережувані або генератори, якщо не захочете. Використовуйте функції та зворотні дзвінки, обіцянки, функції асинхронізації (асинхронізація / очікування) тощо.

Код для простого оповіщення 5с виглядає приблизно так:

const notificationHide = createLogic({
  // the action type that will trigger this logic
  type: 'NOTIFICATION_DISPLAY',
  
  // your business logic can be applied in several
  // execution hooks: validate, transform, process
  // We are defining our code in the process hook below
  // so it runs after the action hit reducers, hide 5s later
  process({ getState, action }, dispatch) {
    setTimeout(() => {
      dispatch({ type: 'NOTIFICATION_CLEAR' });
    }, 5000);
  }
});
    

У моєму РЕПО є більш просунутий приклад сповіщень, який працює аналогічно тому, що описав Себастьян Лорбер, де ви могли обмежити показ на N елементів та обертатись через будь-яку, що вийшла в чергу. приклад сповіщення про редукційну логіку

У мене є різноманітні приклади редукційної логічної jsfiddle, а також повні приклади . Я продовжую працювати над документами та прикладами.

Я хотів би почути ваші відгуки.


Я не впевнений, що мені подобається ваша бібліотека, але мені подобається ваша стаття! Молодці, чоловіче! Ви зробили достатньо роботи, щоб заощадити час інших.
Тайлер Лонг

2
Я створив зразковий проект для редукційної логіки тут: github.com/tylerlong/hello-async/tree/master/redux-logic Я думаю, що це добре розроблений фрагмент програмного забезпечення, і я не бачу великих недоліків порівняно з іншими альтернативи.
Тайлер Лонг

9

Я розумію, що це питання трохи старе, але я збираюся запропонувати інше рішення, використовуючи акаунт, який можна спостерігати за редукцією. Епос.

Цитування офіційної документації:

Що можна спостерігати за редукцією?

Програмне забезпечення для Redux на базі RxJS 5. Створення та скасування асинхронних дій для створення побічних ефектів тощо.

Епіка - це основний примітив спостережуваного відновника.

Це функція, яка приймає потік дій і повертає потік дій. Дії, дії.

Більш-менш словами ви можете створити функцію, яка отримує дії через Потік, а потім повернути новий потік дій (використовуючи загальні побічні ефекти, такі як тайм-аути, затримки, інтервали та запити).

Дозвольте мені опублікувати код, а потім поясніть його трохи більше

store.js

import {createStore, applyMiddleware} from 'redux'
import {createEpicMiddleware} from 'redux-observable'
import {Observable} from 'rxjs'
const NEW_NOTIFICATION = 'NEW_NOTIFICATION'
const QUIT_NOTIFICATION = 'QUIT_NOTIFICATION'
const NOTIFICATION_TIMEOUT = 2000

const initialState = ''
const rootReducer = (state = initialState, action) => {
  const {type, message} = action
  console.log(type)
  switch(type) {
    case NEW_NOTIFICATION:
      return message
    break
    case QUIT_NOTIFICATION:
      return initialState
    break
  }

  return state
}

const rootEpic = (action$) => {
  const incoming = action$.ofType(NEW_NOTIFICATION)
  const outgoing = incoming.switchMap((action) => {
    return Observable.of(quitNotification())
      .delay(NOTIFICATION_TIMEOUT)
      //.takeUntil(action$.ofType(NEW_NOTIFICATION))
  });

  return outgoing;
}

export function newNotification(message) {
  return ({type: NEW_NOTIFICATION, message})
}
export function quitNotification(message) {
  return ({type: QUIT_NOTIFICATION, message});
}

export const configureStore = () => createStore(
  rootReducer,
  applyMiddleware(createEpicMiddleware(rootEpic))
)

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import {configureStore} from './store.js'
import {Provider} from 'react-redux'

const store = configureStore()

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

App.js

import React, { Component } from 'react';
import {connect} from 'react-redux'
import {newNotification} from './store.js'

class App extends Component {

  render() {
    return (
      <div className="App">
        {this.props.notificationExistance ? (<p>{this.props.notificationMessage}</p>) : ''}
        <button onClick={this.props.onNotificationRequest}>Click!</button>
      </div>
    );
  }
}

const mapStateToProps = (state) => {
  return {
    notificationExistance : state.length > 0,
    notificationMessage : state
  }
}

const mapDispatchToProps = (dispatch) => {
  return {
    onNotificationRequest: () => dispatch(newNotification(new Date().toDateString()))
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(App)

Ключовий код для вирішення цієї проблеми такий же простий, як і пиріг, як ви бачите, єдине, що виявляється відмінним від інших відповідей, це функція rootEpic.

Пункт 1. Як і у сагах, вам потрібно поєднувати епоси, щоб отримати функцію верхнього рівня, яка отримує потік дій і повертає потік дій, тож ви можете використовувати її з фабрикою середнього програмного забезпечення createEpicMiddleware . У нашому випадку нам потрібен лише один, тому у нас є тільки rootEpic, тому нам не потрібно нічого комбінувати, але це добре знати факт.

Пункт 2. Наш rootEpic, який піклується про логіку побічних ефектів, займає лише 5 рядків коду, який є приголомшливим! У тому числі і про те, що майже декларативне!

Точка 3. Рядок за рядком rootЕпічне пояснення (у коментарях)

const rootEpic = (action$) => {
  // sets the incoming constant as a stream 
  // of actions with  type NEW_NOTIFICATION
  const incoming = action$.ofType(NEW_NOTIFICATION)
  // Merges the "incoming" stream with the stream resulting for each call
  // This functionality is similar to flatMap (or Promise.all in some way)
  // It creates a new stream with the values of incoming and 
  // the resulting values of the stream generated by the function passed
  // but it stops the merge when incoming gets a new value SO!,
  // in result: no quitNotification action is set in the resulting stream
  // in case there is a new alert
  const outgoing = incoming.switchMap((action) => {
    // creates of observable with the value passed 
    // (a stream with only one node)
    return Observable.of(quitNotification())
      // it waits before sending the nodes 
      // from the Observable.of(...) statement
      .delay(NOTIFICATION_TIMEOUT)
  });
  // we return the resulting stream
  return outgoing;
}

Я сподіваюся, що це допомагає!


Не могли б ви пояснити, що конкретні методи api роблять тут, наприклад switchMap?
Дмитро Зайцев

1
У нашому додатку React Native у Windows ми використовуємо відновлювані функції. Це елегантне рішення для впровадження складної, асинхронної проблеми та має фантастичну підтримку через їхні програми Gitter та проблеми GitHub. Додатковий рівень складності вартий лише того, якщо ви, звичайно, доберетеся до точної проблеми, яку вона мала вирішити.
Метт Харгетт

8

Чому повинно бути так важко? Це просто логіка інтерфейсу. Використовуйте спеціальну дію, щоб встановити дані сповіщень:

dispatch({ notificationData: { message: 'message', expire: +new Date() + 5*1000 } })

і виділений компонент для його відображення:

const Notifications = ({ notificationData }) => {
    if(notificationData.expire > this.state.currentTime) {
      return <div>{notificationData.message}</div>
    } else return null;
}

У цьому випадку питання повинні бути "як ви очистити старий стан?", "Як повідомити компонент, що час змінився"

Ви можете реалізувати деяку дію TIMEOUT, що надсилається на setTimeout з компонента.

Можливо, просто добре його очистити, коли з’являється нове повідомлення.

Як би там не було, setTimeoutдесь, правда? Чому б не зробити це в складі

setTimeout(() => this.setState({ currentTime: +new Date()}), 
           this.props.notificationData.expire-(+new Date()) )

Мотивація полягає в тому, що функціональність "згасання повідомлень" справді є проблемою інтерфейсу користувача. Таким чином, це спрощує тестування на вашу логіку бізнесу.

Здається, немає сенсу перевіряти, як це реалізовано. Це має сенс лише перевірити, коли повідомлення має закінчитися. Таким чином, менше коду для заглушки, більш швидкі тести, чистіший код.


1
Це має бути головна відповідь.
mmla

6

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

Скажімо, ваш творець дії виглядає так:

//action creator
buildAction = (actionData) => ({
    ...actionData,
    timeout: 500
})

тайм-аут може містити кілька значень у вищевказаній дії

  • число в мс - протягом певної тривалості очікування
  • true - для постійної тривалості очікування. (обробляється в середньому програмному забезпеченні)
  • невизначений - для негайного відправлення

Ваша реалізація програмного забезпечення буде виглядати приблизно так:

//timeoutMiddleware.js
const timeoutMiddleware = store => next => action => {

  //If your action doesn't have any timeout attribute, fallback to the default handler
  if(!action.timeout) {
    return next (action)
  }

  const defaultTimeoutDuration = 1000;
  const timeoutDuration = Number.isInteger(action.timeout) ? action.timeout || defaultTimeoutDuration;

//timeout here is called based on the duration defined in the action.
  setTimeout(() => {
    next (action)
  }, timeoutDuration)
}

Тепер ви можете прокласти всі ваші дії через цей проміжний рівень за допомогою redux.

createStore(reducer, applyMiddleware(timeoutMiddleware))

Ви можете знайти кілька подібних прикладів тут


5

Відповідний спосіб це зробити, використовуючи Redux Thunk, який є популярним середнім програмним забезпеченням для Redux, відповідно до документації Redux Thunk:

"Проміжне програмне забезпечення Redux Thunk дозволяє писати творцям дій, які повертають функцію замість дії. Thunk може використовуватися для затримки відправки дії або для відправки лише у випадку, якщо виконано певну умову. Внутрішня функція отримує методи зберігання. відправлення та getState як параметри ".

Таким чином, він в основному повертає функцію, і ви можете затримати відправлення або привести її в стан стану.

Тож щось подібне зробить роботу за вас:

import ReduxThunk from 'redux-thunk';

const INCREMENT_COUNTER = 'INCREMENT_COUNTER';

function increment() {
  return {
    type: INCREMENT_COUNTER
  };
}

function incrementAsync() {
  return dispatch => {
    setTimeout(() => {
      // Yay! Can invoke sync or async actions with `dispatch`
      dispatch(increment());
    }, 5000);
  };
}

4

Це просто. Скористайтеся пакетом trim-redux і напишіть так у тому componentDidMountчи іншому місці та вбийте його componentWillUnmount.

componentDidMount() {
  this.tm = setTimeout(function() {
    setStore({ age: 20 });
  }, 3000);
}

componentWillUnmount() {
  clearTimeout(this.tm);
}

3

Сам Redux - це досить багатослівна бібліотека, і для таких матеріалів вам доведеться використовувати щось на зразок Redux-thunk , що дастьdispatch функцію, тож ви зможете відправити закриття сповіщення через кілька секунд.

Я створив бібліотеку для вирішення таких питань, як багатослівність та компонування, і ваш приклад буде виглядати наступним чином:

import { createTile, createSyncTile } from 'redux-tiles';
import { sleep } from 'delounce';

const notifications = createSyncTile({
  type: ['ui', 'notifications'],
  fn: ({ params }) => params.data,
  // to have only one tile for all notifications
  nesting: ({ type }) => [type],
});

const notificationsManager = createTile({
  type: ['ui', 'notificationManager'],
  fn: ({ params, dispatch, actions }) => {
    dispatch(actions.ui.notifications({ type: params.type, data: params.data }));
    await sleep(params.timeout || 5000);
    dispatch(actions.ui.notifications({ type: params.type, data: null }));
    return { closed: true };
  },
  nesting: ({ type }) => [type],
});

Таким чином, ми створюємо дії синхронізації для показу сповіщень всередині дії async, які можуть запитати деяку інформацію фону або перевірити пізніше, чи було повідомлення закрито вручну.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.