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

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

Ilja

Ответы (13)

Не попадайтесь в ловушку , думая, что библиотека должна предписывать, как все делать. Если вы хотите что-то сделать с таймаутом в JavaScript, вам нужно использовать setTimeout. Нет причин, по которым действия Redux должны отличаться.

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

Запись встроенного асинхронного кода

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

До сих пор мы не использовали какое-либо промежуточное ПО или другие передовые концепции.

Извлечение создателя асинхронных действий

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

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

Если бы у вас было одноэлементное хранилище, экспортированное из какого-то модуля, вы могли бы просто импортировать его и отправить прямо в него вместо этого:

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

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

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

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

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

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

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

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

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

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

Это было мотивацией для найти способ «узаконить» этот шаблон предоставления отправки вспомогательной функции и помочь 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 предоставит ему отправку в качестве аргумента. Он также «проглатывает» такие действия, поэтому не беспокойтесь о том, что ваши редукторы получат странные аргументы функции. Ваши редукторы будут получать только действия с простыми объектами - либо напрямую, либо выполняемые функциями, как мы только что описали.

Это не выглядит очень полезным, правда? Не в этой конкретной ситуации. Однако он позволяет нам объявить 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)
  }
}

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

Как бы мы использовали его в нашем компоненте? Определенно, мы могли бы написать это:

// component.js
showNotificationWithTimeout('You just logged in.')(this.props.dispatch)

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

Однако это еще более неудобно, чем оригинальная версия! Почему мы вообще пошли этим путем?

Из-за того, что я вам говорил раньше.Если промежуточное ПО Redux Thunk включено, каждый раз, когда вы пытаетесь отправить функцию вместо объекта действия, промежуточное ПО будет вызывать эту функцию с помощью самого метода dispatch в качестве первого аргумента.

Итак, мы можем сделать это вместо:

// component.js
this.props.dispatch(showNotificationWithTimeout('You just logged in.'))

Наконец, отправка асинхронного действия (на самом деле, серии действий) ничем не отличается от отправки одного действия синхронно компоненту. Это хорошо, потому что компонентам не должно быть дела до того, происходит что-то синхронно или асинхронно. Мы просто абстрагировали это.

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

// 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 , который их использует.

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

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

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

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

store.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
  store.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)

Не переживайте, если не знаете, зачем вы это делаете.

Using Redux-saga

As Dan Abramov said, if you want more advanced control over your async code, you might take a look at redux-saga.

This answer is a simple example, if you want better explanations on why redux-saga can be useful for your application, check this other answer.

The general idea is that Redux-saga offers an ES6 generators interpreter that permits you to easily write async code that looks like synchronous code (this is why you'll often find infinite while loops in Redux-saga). Somehow, Redux-saga is building its own language directly inside Javascript. Redux-saga can feel a bit difficult to learn at first because you need a basic understanding of generators, but also understand the language offered by Redux-saga.

I'll try here to describe here the notification system I built on top of redux-saga. This example currently runs in production.

Advanced notification system specification

  • You can request a notification to be displayed
  • You can request a notification to hide
  • A notification should not be displayed more than 4 seconds
  • Multiple notifications can be displayed at the same time
  • No more than 3 notifications can be displayed at the same time
  • If a notification is requested while there are already 3 displayed notifications, then queue/postpone it.

Result

Screenshot of my production app Stample.co

toasts

Code

Here I named the notification a toast but this is a naming detail.

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

And the reducer:

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

Usage

You can simply dispatch TOAST_DISPLAY_REQUESTED events. If you dispatch 4 requests, only 3 notifications will be displayed, and the 4th one will appear a bit later once the 1st notification disappears.

Note that I don't specifically recommend dispatching TOAST_DISPLAY_REQUESTED from JSX. You'd rather add another saga that listens to your already-existing app events, and then dispatch the TOAST_DISPLAY_REQUESTED: your component that triggers the notification, does not have to be tightly coupled to the notification system.

Conclusion

My code is not perfect but runs in production with 0 bugs for months. Redux-saga and generators are a bit hard initially but once you understand them this kind of system is pretty easy to build.

It's even quite easy to implement more complex rules, like:

  • when too many notifications are "queued", give less display-time for each notification so that the queue size can decrease faster.
  • detect window size changes, and change the maximum number of displayed notifications accordingly (for example, desktop=3, phone portrait = 2, phone landscape = 1)

Honestly, good luck implementing this kind of stuff properly with thunks.

Note you can do exactly the same kind of thing with redux-observable which is very similar to redux-saga. It's almost the same and is a matter of taste between generators and RxJS.

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

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

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

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

createStore(reducer, applyMiddleware(timeoutMiddleware))

Вы можете найти похожие примеры здесь

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

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

  1. Встроенная запись асинхронного кода
  2. Извлечение создателя асинхронных действий
  3. Использовать Redux Thunk
  4. Использовать Redux Saga

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

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

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

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

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

Redux Saga

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

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

I understand that this question is a bit old but I'm going to introduce another solution using redux-observable aka. Epic.

Quoting the official documentation:

What is redux-observable?

RxJS 5-based middleware for Redux. Compose and cancel async actions to create side effects and more.

An Epic is the core primitive of redux-observable.

It is a function which takes a stream of actions and returns a stream of actions. Actions in, actions out.

In more or less words, you can create a function that receives actions through a Stream and then return a new stream of actions (using common side effects such as timeouts, delays, intervals, and requests).

Let me post the code and then explain a bit more about it

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(
  
    
  ,
  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 (
      
{this.props.notificationExistance ? (

{this.props.notificationMessage}

) : ''}
); } } 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)

The key code to solve this problem is as easy as pie as you can see, the only thing that appears different from the other answers is the function rootEpic.

Point 1. As with sagas, you have to combine the epics in order to get a top level function that receives a stream of actions and returns a stream of actions, so you can use it with the middleware factory createEpicMiddleware. In our case we only need one so we only have our rootEpic so we don't have to combine anything but it's a good to know fact.

Point 2. Our rootEpic which takes care about the side effects logic only takes about 5 lines of code which is awesome! Including the fact that is pretty much declarative!

Point 3. Line by line rootEpic explanation (in comments)

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

I hope it helps!

Redux itself is a pretty verbose library, and for such stuff you would have to use something like Redux-thunk, which will give a dispatch function, so you will be able to dispatch closing of the notification after several seconds.

I have created a library to address issues like verbosity and composability, and your example will look like the following:

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

So we compose sync actions for showing notifications inside async action, which can request some info the background, or check later whether the notification was closed manually.

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

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

и специальный компонент для его отображения:

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

В этом случае должны быть вопросы «как очистить старое состояние?», «Как уведомить компонент об изменении времени»

Вы можете реализовать некоторое действие TIMEOUT, которое отправляется в setTimeout из компонента.

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

Все равно где-нибудь должно быть setTimeout, да? Почему бы не сделать это в компоненте

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

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

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

Вы можете сделать это с помощью redux-thunk. В документе redux есть руководство для асинхронных действий, таких как setTimeout.

Это просто. Используйте пакет trim-redux и напишите так в componentDidMount или другом месте и убейте его в componentWillUnmount.

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

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

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

В итоге я использовал setTimeout () в компоненте , чтобы затем он мог вызывать и отправлять действие REMOVE при заданном идентификатор.

export function Alert(props: Props) {
  useEffect(() => {
    const timeoutID = setTimeout(() => {
      dispatchAction({
        type: REMOVE,
        payload: {
          id: id,
        },
      });
    }, timeout ?? 2000);
    return () => clearTimeout(timeoutID);
  }, []);
  return ;
}

Подходящий способ сделать это - использовать Redux Thunk, который является популярное промежуточное ПО для Redux, согласно документации Redux Thunk:

"Промежуточное ПО Redux 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);
  };
}

Я бы порекомендовал также взглянуть на шаблон 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.

• 100001 Я поместил свою бизнес-логику в приложение React / Redux?

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

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

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

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

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

const notificationHide = createLogic ({
  // тип действия, которое запустит эту логику
  тип: 'NOTIFICATION_DISPLAY',
  
  // ваша бизнес-логика может применяться в нескольких
  // хуки выполнения: проверка, преобразование, обработка
  // Мы определяем наш код в хуке процесса ниже
  // чтобы он запускался после того, как действие попало в редукторы, скрыть через 5 секунд
  процесс ({getState, действие}, отправка) {
    setTimeout (() => {
      отправка ({тип: 'NOTIFICATION_CLEAR'});
    }, 5000);
  }
});
    

• 100001пример уведомления redux-logic

У меня есть множество живых примеров redux-logic jsfiddle, а также полных примеров. Продолжаю работать над документами и примерами.

Я хотел бы услышать ваш отзыв.

2022 WebDevInsider