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

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

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

Ответ 1

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

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

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 doesnt 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(). Вот почему вы можете вернуть Promise из thunk и дождаться его завершения, вызвав dispatch(someThunkReturningPromise()).then(...).

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

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

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

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

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

Ответ 2

Использование 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

toasts

Код

Здесь я назвал уведомление 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 really important to you, in my case it 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 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-й появится чуть позже, как только исчезнет 1-е уведомление.

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

Заключение

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

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

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

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

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

Ответ 3

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

В настоящее время существует четыре примера проектов:

Принятый ответ является удивительным.

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

  • Никаких runnable примеров проектов, только некоторые фрагменты кода.
  • Нет образца кода для других альтернатив, например:

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

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

Редсовая сага

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

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

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

Против

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

Обратитесь к runnable project, если приведенные выше фрагменты кода не отвечают на все ваши вопросы.

Ответ 4

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

Ответ 5

Я бы порекомендовал также взглянуть на шаблон SAM.

Шаблон SAM защищает включение "предиката следующего действия", в котором (после автоматического обновления модели (модель SAM ~ состояние редуктора + хранилище) активируются (автоматические) действия, такие как "уведомления автоматически исчезают через 5 секунд" ).

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

Так, например, код,

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

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

не будет разрешено с помощью SAM, потому что тот факт, что действие hideNotification может быть отправлено, зависит от модели, успешно принимающей значение "showNotication: true". Могут быть другие части модели, которые мешают ей принять ее, и поэтому не было бы причин запускать действие hideNotification.

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

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

Ответ 6

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

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

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

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

Это освобождает вас от реализации вашей основной бизнес-логики, как вам нравится. Вам не нужно использовать наблюдаемые или генераторы, если вы этого не хотите. Используйте функции и обратные вызовы, promises, асинхронные функции (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);
  }
});
    

Ответ 7

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

Указание официальной документации:

Что такое редукция?

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

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

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

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

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

store.js

  import {createStore, applyMiddleware} из 'redux'
import {createEpicMiddleware} из 'redux-observable'
import {Observable} из '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} = действие console.log(тип) переключатель (тип) {   case NEW_NOTIFICATION:     обратное сообщение   ломать   case QUIT_NOTIFICATION:     return initialState   ломать }
 возвращаемое состояние
}

const rootEpic = (действие $) = > { const incoming = действие $.ofType(NEW_NOTIFICATION) const outgoing = incoming.switchMap((action) = > {   return Observable.of(quitNotification())     .delay(NOTIFICATION_TIMEOUT)     //.takeUntil(action$.ofType(NEW_NOTIFICATION)) });
 возвращать исходящие;
}

функция экспорта newNotification (message) { return ({type: NEW_NOTIFICATION, message})
}
функция экспорта quitNotification (message) { return ({type: QUIT_NOTIFICATION, message});
}

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

index.js

  import React from 'react';
импортировать ReactDOM из "реагирования";
импортировать приложение из './App';
import {configureStore} из './store.js'
import {Provider} из 'react-redux'

const store = configureStore()

ReactDOM.render( < Магазин поставщика = {store} >   < App/> </& Провайдер GT;, document.getElementById( 'корень')
);
Код>

App.js

  import React, {Component} из 'response';
импортировать {connect} из 'react-redux'
импортировать {newNotification} из './store.js'

class App расширяет компонент {
 render() {   вернуть (     < div className = "App" >       {this.props.notificationExistance? (<p> {this.props.notificationMessage} </p>): ''}       < button onClick = {this.props.onNotificationRequest} > Нажмите кнопку </button>     </дел >   ); }
}

const mapStateToProps = (state) = > { вернуть {   notificationExistance: state.length > 0,   messageMessage: состояние }
}

const mapDispatchToProps = (отправка) = > { вернуть {   onNotificationRequest:() = > отправка (newNotification (новая дата(). toDateString())) }
}

экспорт по умолчанию connect (mapStateToProps, mapDispatchToProps) (приложение)
Код>

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

Точка 1. Как и в случае с сагами, вы должны объединить эпики, чтобы получить функцию верхнего уровня, которая получает поток действий и возвращает поток действий, поэтому вы можете использовать его с промежуточным программным обеспечением factory createEpicMiddleware. В нашем случае нам нужен только один, поэтому у нас есть только rootEpic, поэтому нам не нужно ничего комбинировать, но хорошо знать факт.

Точка 2. Наш rootEpic, который заботится о логике побочных эффектов, занимает всего около 5 строк кода, что является удивительным! Включая тот факт, что в значительной степени декларативный!

Точка 3. Объяснение строки в строке rootEpic (в комментариях)

  const rootEpic = (действие $) = > { // устанавливает входящую константу как поток // действий с типом NEW_NOTIFICATION const incoming = действие $.ofType(NEW_NOTIFICATION) // Слияние "входящего" потока с потоком, получаемым для каждого вызова // Эта функциональность похожа на flatMap (или Promise.all в некотором роде) // Создает новый поток со значениями входящих и // Полученные значения потока, генерируемого переданной функцией // но он останавливает слияние, когда входящий получает новое значение SO!, // в результате: в результирующем потоке не задано действие quitNotification // в случае появления нового предупреждения const outgoing = incoming.switchMap((action) = > {   // создает наблюдаемые с переданным значением   // (поток с одним узлом)   return Observable.of(quitNotification())     // он ждет перед отправкой узлов     // из инструкции Observable.of(...)     .delay(NOTIFICATION_TIMEOUT) }); // возвращаем результирующий поток возвращать исходящие;
}
Код>

Надеюсь, это поможет!

Ответ 8

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

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

 //action creator
buildAction = (actionData) = > ({   ... actionData,   таймаут: 500
})
Код>

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

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

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

<Предварительно > <код > //timeoutMiddleware.js const timeoutMiddleware = store = > next = > action = > { // Если ваше действие не имеет атрибута тайм-аута, вернитесь к обработчику по умолчанию if (! action.timeout) {   return next (action) } const defaultTimeoutDuration = 1000; const timeoutDuration = Number.isInteger(action.timeout)? action.timeout || defaultTimeoutDuration; //timeout здесь вызывается в зависимости от продолжительности, определенной в действии. setTimeout (() = > {   следующий (действие) }, timeoutDuration) } Код >

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

  createStore (редуктор, applyMiddleware (timeoutMiddleware))
Код>

Здесь вы можете найти несколько подобных примеров здесь

Ответ 9

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

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()) )

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

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

Ответ 10

Соответствующий способ сделать это - использовать 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);
  };
}

Ответ 11

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

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

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

Ответ 12

Редкс сам по себе является довольно многословной библиотекой, и для таких вещей вам нужно будет использовать что-то вроде 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],
});

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

Ответ 13

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

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