Как отправить действие Redux с таймаутом?

891

У меня есть действие, которое обновляет состояние уведомления моего приложения. Обычно это уведомление будет ошибкой или какой-либо информацией. Мне нужно затем через 5 секунд отправить еще одно действие, которое вернет исходное состояние уведомления, поэтому уведомления нет. Основная причина этого заключается в обеспечении функциональности, при которой уведомления автоматически исчезают через 5 секунд.

Мне не повезло с использованием setTimeoutи возвратом другого действия, и я не могу найти, как это делается в Интернете. Так что любые советы приветствуются.

Ilja
источник
30
Не забудьте проверить мой redux-sagaоснованный ответ, если вы хотите что-то лучше, чем гром. Поздний ответ, так что вам придется долго прокручивать его перед тем, как его увидеть :) Это не значит, что читать не стоит. Вот кратчайший путь: stackoverflow.com/a/38574266/82609
Себастьян Лорбер
5
Всякий раз, когда вы делаете setTimeout, не забывайте очищать таймер, используя clearTimeout в методе жизненного цикла componentWillUnMount
Hemadri Dasari
2
Reduce-saga - это круто, но, похоже, они не поддерживают типизированные ответы от функций генератора. Может иметь значение, если вы используете машинопись с реагировать.
Кристиан Рамирес

Ответы:

2620

Не попадайтесь в ловушку, думая, что библиотека должна прописывать, как все делать . Если вы хотите что-то сделать с тайм-аутом в 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 Creator

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

  • Это заставляет вас дублировать эту логику везде, где вы хотите показать уведомление.
  • У уведомлений нет идентификаторов, поэтому у вас будет состояние гонки, если вы покажете два уведомления достаточно быстро. Когда первый тайм-аут заканчивается, он отправляет 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.')    

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

Магазин Singleton также усложняет тестирование. Вы больше не можете издеваться над магазином при тестировании создателей действий, поскольку они ссылаются на конкретный реальный магазин, экспортированный из определенного модуля. Вы даже не можете сбросить его состояние снаружи.

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

Возвращаясь к предыдущей версии:

// 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.')    

Это решает проблемы с дублированием логики и спасает нас от условий гонки.

Thunk Middleware

Для простых приложений подход должен быть достаточным. Не беспокойтесь о промежуточном программном обеспечении, если вы довольны им.

В больших приложениях, однако, вы можете найти определенные неудобства вокруг него.

Например, кажется неудачным, что мы должны пройти мимо 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)

Состояние чтения в Thunks

Обычно ваши редукторы содержат бизнес-логику для определения следующего состояния. Однако редукторы включаются только после отправки действий. Что если у вас есть побочный эффект (например, вызов API) в создателе thunk action, и вы хотите предотвратить его при определенных условиях?

Без использования промежуточного программного обеспечения Thunk, вы просто выполните эту проверку внутри компонента:

// component.js
if (this.props.areNotificationsEnabled) {
  showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')
}

Тем не менее, целью извлечения создателя действия была централизация этой повторяющейся логики во многих компонентах. К счастью, Redux Thunk предлагает вам прочитать текущее состояние магазина Redux. В дополнение к dispatchэтому он также передается getStateв качестве второго аргумента функции, которую вы возвращаете создателю thunk action. Это позволяет thunk прочитать текущее состояние магазина.

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() только условную диспетчеризацию различных действий, вместо этого поместить бизнес-логику в редукторы.

Следующие шаги

Теперь, когда у вас есть базовая интуиция о том, как работают thunks, посмотрите пример асинхронного Redux, который их использует.

Вы можете найти много примеров, в которых thunks возвращают обещания. Это не обязательно, но может быть очень удобно. Redux не заботится о том, что вы возвращаете из thunk, но дает вам возвращаемое значение dispatch(). Вот почему вы можете вернуть Обещание из thunk и дождаться его завершения, позвонив dispatch(someThunkReturningPromise()).then(...).

Вы также можете разделить сложных создателей Thunk Action на несколько меньших создателей Thunk Action. dispatchМетод , предоставляемый санков может принять санки самого по себе, так что вы можете применить шаблон рекурсивно. Опять же, это лучше всего работает с Promises, потому что вы можете реализовать асинхронный поток управления поверх этого.

Для некоторых приложений вы можете оказаться в ситуации, когда ваши требования к потоку асинхронного управления слишком сложны, чтобы их можно было выразить с помощью групповых символов. Например, повторная попытка неудачных запросов, поток повторной авторизации с токенами или пошаговая регистрация могут быть слишком многословными и подверженными ошибкам при написании таким образом. В этом случае вы можете захотеть взглянуть на более продвинутые решения асинхронного потока управления, такие как 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-саги

Как сказал Дан Абрамов, если вы хотите более расширенный контроль над вашим асинхронным кодом, вы можете взглянуть на redux-saga .

Этот ответ является простым примером. Если вы хотите получить более подробные объяснения того, почему redux-saga может быть полезен для вашего приложения, проверьте этот другой ответ .

Общая идея заключается в том, что Redux-saga предлагает интерпретатор генераторов ES6, который позволяет вам легко писать асинхронный код, который выглядит как синхронный код (вот почему вы часто найдете бесконечные циклы while в Redux-saga). Каким-то образом Redux-saga создает свой собственный язык прямо внутри Javascript. Поначалу Redux-saga может показаться немного сложным в изучении, потому что вам нужно базовое понимание генераторов, но также и понимание языка, предлагаемого Redux-saga.

Я постараюсь здесь описать систему уведомлений, которую я построил на основе redux-saga. Этот пример в настоящее время работает в производстве.

Спецификация расширенной системы уведомлений

  • Вы можете запросить уведомление для отображения
  • Вы можете запросить уведомление, чтобы скрыть
  • Уведомление не должно отображаться более 4 секунд
  • Несколько уведомлений могут отображаться одновременно
  • Одновременно может отображаться не более 3 уведомлений.
  • Если уведомление запрашивается, когда уже есть 3 отображаемых уведомления, то поставьте его в очередь / отложите.

Результат

Скриншот моего производственного приложения Stample.co

тосты

Код

Здесь я назвал уведомление a, 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 уведомления, а четвертый появится чуть позже, как только исчезнет 1-е уведомление.

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

Вывод

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

Даже довольно легко реализовать более сложные правила, такие как:

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

Честно говоря, удачи в правильной реализации такого рода вещей с помощью thunks.

Заметьте, что вы можете делать точно такие же вещи с redux-observable, что очень похоже на redux-saga. Это почти то же самое, и дело вкуса между генераторами и RxJS.

Себастьян Лорбер
источник
18
Я хотел бы, чтобы ваш ответ пришел раньше, когда был задан вопрос, потому что я не могу больше согласиться с использованием библиотеки побочных эффектов Saga для бизнес-логики, подобной этой. Редукторы и Создатели Действий предназначены для переходов между состояниями. Рабочие процессы не совпадают с функциями перехода состояний. Рабочие процессы проходят через переходы, но не являются самими переходами. Redux + React этого не хватает сами по себе - именно поэтому Redux Saga так полезна.
Аттик
4
Спасибо, я стараюсь сделать так, чтобы по этой причине популярность Sagate-Saga стала популярной :) Слишком мало людей думают, что в настоящее время Lagber-Saga является просто заменой Thunks, и не понимают, как Sasebion Saga позволяет создавать сложные и не связанные между собой рабочие процессы
Себастьен Лорбер
1
Точно. Действия и редукторы являются частью конечного автомата. Иногда для сложных рабочих процессов вам нужно что-то еще для управления конечным автоматом, который не является частью самого конечного автомата!
Аттик
2
Действия: полезные данные / события в переходное состояние. Редукторы: функции перехода состояний. Компоненты: пользовательские интерфейсы, отражающие состояние. Но здесь не хватает одной важной части - как вы управляете процессом многих переходов, каждый из которых имеет свою собственную логику, определяющую, какой переход выполнить следующим? Redux Saga!
Аттик
2
@mrbrdo, если вы внимательно прочитаете мой ответ, вы заметите, что тайм-ауты уведомлений фактически обрабатываются yield call(delay,timeoutValue);: это не тот же API, но он имеет тот же эффект
Себастьен Лорбер,
25

Репозиторий с примерами проектов

На данный момент существует четыре примера проектов:

  1. Написание асинхронного кода Inline
  2. Извлечение Async Action Creator
  3. Используйте Redux Thunk
  4. Используйте Redux Saga

Принятый ответ потрясающий.

Но чего-то не хватает:

  1. Нет запускаемых примеров проектов, только некоторые фрагменты кода.
  2. Нет примера кода для других альтернатив, таких как:
    1. Redux Saga

Поэтому я создал репозиторий Hello Async, чтобы добавить недостающие элементы:

  1. Работающие проекты. Вы можете скачать и запустить их без изменений.
  2. Предоставьте пример кода для большего количества альтернатив:

Redux Saga

Принятый ответ уже содержит примеры фрагментов кода для 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

Pros

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

Cons

  • Это зависит от генераторов ES6, который является относительно новым.

Пожалуйста, обратитесь к готовому проекту, если приведенные выше фрагменты кода не отвечают на все ваши вопросы.

Тайлер Лонг
источник
23

Вы можете сделать это с помощью redux-thunk . В избыточном документе есть руководство по асинхронным действиям, таким как 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, потому что тот факт, что действие hideNotification может быть отправлено, зависит от модели, успешно принимающей значение "showNotication: true". Могут быть другие части модели, которые не позволяют ей принять его, и, следовательно, не будет причин для запуска действия hideNotification.

Я настоятельно рекомендую реализовать надлежащий предикат следующего действия после того, как магазин обновит и новое состояние управления моделью станет известно. Это самый безопасный способ реализовать поведение, которое вы ищете.

Вы можете присоединиться к нам на Gitter, если хотите. Здесь также есть руководство по началу работы с SAM .

Жан-Жак Дубрей
источник
Я пока только поцарапал поверхность, но уже в восторге от паттерна SAM. V = S( vm( M.present( A(data) ) ), nap(M))это просто красиво. Спасибо, что поделились своими мыслями и опытом. Я буду копать глубже.
@ftor, спасибо! когда я написал это в первый раз, у меня было такое же чувство. Я использую SAM в производстве уже почти год, и я не могу вспомнить время, когда я чувствовал, что мне нужна библиотека для реализации SAM (даже vdom, хотя я вижу, когда это можно использовать). Всего одна строка кода, вот и все! SAM производит изоморфный код, нет никакой двусмысленности, где, как обращаться с асинхронными вызовами ... Я не могу думать о времени, когда я, хотя, что я делаю?
метапрограммист
SAM - это настоящий паттерн разработки программного обеспечения (только что с ним был создан Alexa SDK). Он основан на TLA + и пытается донести всю мощь этой невероятной работы до каждого разработчика. SAM исправляет три приближения, которые (в значительной степени) используются каждым в течение десятилетий: - действия могут манипулировать состоянием приложения - назначения эквивалентны мутации - нет точного определения того, что такое шаг программирования (например, a = b * ca step , 1 / читать b, c 2 / вычислить b * c, 3 / назначить с результатом три различных шага?
метапрограммист
20

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

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

Он позволяет вам перехватывать действия для проверки, подтверждения, авторизации, а также предоставлять способ выполнения асинхронного ввода-вывода.

Некоторые общие функции могут быть просто объявлены как отмена, регулирование, отмена и только использование ответа от последнего запроса (takeLatest). Избыточная логика оборачивает ваш код, предоставляя вам эту функциональность.

Это освобождает вас от реализации вашей основной бизнес-логики так, как вам нравится. Вам не нужно использовать наблюдаемые или генераторы, если вы не хотите. Используйте функции и обратные вызовы, обещания, асинхронные функции (async / await) и т. Д.

Код для простого уведомления 5s будет выглядеть примерно так:

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

Я понимаю, что этот вопрос немного устарел, но я собираюсь представить другое решение, использующее избыточно наблюдаемый aka. Эпическая.

Цитирование официальной документации:

Что такое наблюдаемый редукс?

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

Эпос является основным примитивом наблюдаемого редукса.

Это функция, которая принимает поток действий и возвращает поток действий. Действия в, действия вне.

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

Позвольте мне опубликовать код, а затем объяснить немного больше об этом

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. Построчно rootEpic объяснение (в комментариях)

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;
}

Я надеюсь, что это помогает!

cnexans
источник
Не могли бы вы объяснить, что здесь делают конкретные методы 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()) )

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

Кажется, нет смысла проверять, как это реализовано. Имеет смысл проверить, когда время ожидания уведомления истекло. Таким образом, меньше кода на заглушку, быстрее тесты, более чистый код.

Vanuan
источник
1
Это должен быть главный ответ.
mmla
6

Если вы хотите обработать тайм-аут при выборочных действиях, вы можете попробовать промежуточное ПО подход . Я столкнулся с аналогичной проблемой для выборочной обработки действий, основанных на обещаниях, и это решение было более гибким.

Допустим, ваш создатель действий выглядит так:

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

Тайм-аут может содержать несколько значений в вышеуказанном действии

  • число в мс - в течение определенного времени ожидания
  • true - для постоянной продолжительности тайм-аута. (обрабатывается в промежуточном программном обеспечении)
  • undefined - для немедленной отправки

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

//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)
}

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

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],
});

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

Bloomca
источник