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


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

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

13 673

13 ответов:

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

Если вам не нравится делать опечатки при отправке одних и тех же действий из разных компонентов, вы можете извлечь создателей действий вместо отправки объектов действий inline:

// 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 interval ID and call
  // clearInterval(), 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.')    

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

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

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

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

// 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() на самом деле не является создателем действий. Он не возвращает действий "возвращение".

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

это было мотивацией для найти способ "узаконить" эту схему предоставления dispatch к вспомогательной функции и помогите Redux "увидеть" таких создателей асинхронных действий как частный случай обычных создателей действий а не совсем другой функции.

если вы все еще с нами и вы узнаете как проблема в вашем приложении, вы можете использовать Redux Thunk middleware.

в сущности, 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)
})

когда это промежуточное программное обеспечение включено,если вы отправляете функцию, Перевождь промежуточного преобразователь даст ему 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 middleware включен, каждый раз, когда вы пытаетесь отправить функцию вместо объекта действия, middleware будет вызывать эту функцию с помощью dispatch сам метод в качестве первого аргумента.

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

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

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

обратите внимание, что с тех пор, как мы "научили" Redux распознавать таких" особых " создателей действий (мы называем их подумать action creators), теперь мы можем использовать их в любом месте, где мы будем использовать обычных создателей действий. Например, мы можем использовать их с connect():

// actions.js

function showNotification(id, text) {
  return { type: 'SHOW_NOTIFICATION', id, text }
}
function hideNotification(id) {
  return { type: 'HIDE_NOTIFICATION', id }
}

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

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

// component.js

import { connect } from 'react-redux'

// ...

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

// ...

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

состояние чтения в Преобразователи

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

без использования thunk middleware, вы бы просто сделать эту проверку внутри компонента:

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

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

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

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

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

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

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

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

используя Redux-saga

как сказал Дэн Абрамов, если вы хотите более продвинутый контроль над своим асинхронным кодом, вы можете взглянуть на 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

код

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

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

вывод

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

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

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

Honnestly, удачи реализации такого рода вещи должным образом с thunks.

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

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

шаблон SAM выступает за включение "следующего действия-предиката", где (автоматические) действия, такие как" уведомления автоматически исчезают через 5 секунд", запускаются после обновления модели (SAM model ~ reducer state + store).

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

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

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

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

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

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

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

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

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

текущий есть четыре образца проектов:

  1. Написание Асинхронного Кода Inline
  2. Извлечение Создателя Асинхронного Действия
  3. Использовать Redux Thunk
  4. Используйте Redux Saga

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

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

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

поэтому я создал Привет Асинхронного репозиторий для добавления недостающих вещей:

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

Redux Saga

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

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

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

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

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

некоторые общие функции могут быть просто объявлены, как debouncing, дросселирование, отмена, и только с помощью ответа от последнего запроса (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 элементов и повернуть через любой из них в очереди вверх. redux-logic notification example

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

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

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

цитируя официальную документацию:

что такое redux-observable?

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

эпос является основным примитивом redux-observable.

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

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

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

магазине.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))
)
.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')
);

приложение.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. Построчное рутепическое объяснение (in комментарии)

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

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

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

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

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

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

  • число в мс - для определенной продолжительности таймаута
  • правда - за постоянного ожидания продолжительность. (обрабатывается в промежуточном программном обеспечении)
  • 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))

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

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

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

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

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

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

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

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

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

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

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

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

таким образом, в основном он возвращает функцию, и вы можете отложить отправку или поместить ее в состояние состояния.

Так что что-то вроде этого будет делать работу за вас:

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

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

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

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

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

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

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

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

componentWillUnMount(){
   clearTimeout(this.timeout);
}