Jak wysłać akcję Redux z limitem czasu?


891

Mam działanie, które aktualizuje stan powiadomienia mojej aplikacji. Zwykle to powiadomienie będzie pewnego rodzaju błędem lub informacją. Muszę następnie wysłać inną akcję po 5 sekundach, która przywróci stan powiadomienia do początkowej, więc nie będzie żadnego powiadomienia. Głównym powodem tego jest zapewnienie funkcjonalności, w której powiadomienia znikają automatycznie po 5 sekundach.

Nie miałem szczęścia z użyciem setTimeouti zwrotem innej akcji i nie mogę znaleźć sposobu, w jaki odbywa się to online. Wszelkie porady są mile widziane.


30
Nie zapomnij sprawdzić mojej redux-sagaodpowiedzi, jeśli chcesz czegoś lepszego niż gromady. Późna odpowiedź, więc musisz przewijać dużo czasu, zanim zobaczysz, że się pojawia :) nie oznacza, że ​​nie warto czytać. Oto skrót: stackoverflow.com/a/38574266/82609
Sebastien Lorber

5
Ilekroć wybierzesz setTimeout, nie zapomnij wyczyścić timera za pomocą clearTimeout w metodzie cyklu życia componentWillUnMount
Hemadri Dasari

2
redux-saga jest fajna, ale wydaje się, że nie mają wsparcia dla typowanych odpowiedzi z funkcji generatora. Może to mieć znaczenie, jeśli używasz maszynopisu z reakcją.
Crhistian Ramirez,

Odpowiedzi:


2616

Nie wpadnij w pułapkę myślenia, że ​​biblioteka powinna określać, jak zrobić wszystko . Jeśli chcesz zrobić coś z limitem czasu w JavaScript, musisz tego użyć setTimeout. Nie ma powodu, dla którego działania Redux powinny być inne.

Redux nie oferują kilka alternatywnych sposobów radzenia sobie z asynchronicznym rzeczy, ale należy używać tylko tych, kiedy zdajesz sobie sprawę, powtarzamy za dużo kodu. Jeśli nie masz tego problemu, skorzystaj z oferty języka i wybierz najprostsze rozwiązanie.

Pisanie kodu asynchronicznego w linii

To zdecydowanie najprostszy sposób. I tutaj nie ma nic specyficznego dla Redux.

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

Podobnie z wnętrza podłączonego komponentu:

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

Jedyną różnicą jest to, że w podłączonym komponencie zwykle nie masz dostępu do samego sklepu, ale dostajesz jednego dispatch()lub konkretnych twórców akcji wstrzykiwanych jako rekwizyty. Nie ma to jednak dla nas żadnej różnicy.

Jeśli nie lubisz tworzyć literówek podczas wywoływania tych samych akcji z różnych komponentów, możesz wyodrębnić twórców akcji zamiast wywoływać obiekty akcji bezpośrednio:

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

Lub, jeśli wcześniej związałeś je connect():

this.props.showNotification('You just logged in.')
setTimeout(() => {
  this.props.hideNotification()
}, 5000)

Do tej pory nie korzystaliśmy z oprogramowania pośredniego ani innych zaawansowanych koncepcji.

Wyodrębnianie Async Action Creator

Powyższe podejście działa dobrze w prostych przypadkach, ale może się okazać, że ma kilka problemów:

  • Zmusza cię do zduplikowania tej logiki gdziekolwiek chcesz pokazać powiadomienie.
  • Powiadomienia nie mają identyfikatorów, więc będziesz mieć wyścig, jeśli wystarczająco szybko wyświetlisz dwa powiadomienia. Po HIDE_NOTIFICATIONupływie pierwszego limitu czasu nastąpi wysłanie , które błędnie ukrywa drugie powiadomienie wcześniej niż po upływie limitu czasu.

Aby rozwiązać te problemy, musisz wyodrębnić funkcję, która scentralizuje logikę limitu czasu i wywoła te dwie akcje. Może to wyglądać tak:

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

Teraz komponenty mogą korzystać showNotificationWithTimeoutbez powielania tej logiki lub posiadania warunków wyścigu z różnymi powiadomieniami:

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

// otherComponent.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged out.')    

Dlaczego showNotificationWithTimeout()akceptuje dispatchjako pierwszy argument? Ponieważ musi wysłać działania do sklepu. Zwykle komponent ma do niego dostęp, dispatchale ponieważ chcemy, aby funkcja zewnętrzna przejmowała kontrolę nad wysyłaniem, musimy dać mu kontrolę nad wysyłaniem.

Jeśli wyeksportowałeś sklep z singletonem z jakiegoś modułu, możesz go po prostu zaimportować i dispatchbezpośrednio na nim:

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

Wygląda to na prostsze, ale nie zalecamy tego podejścia . Głównym powodem, dla którego nam się nie podoba, jest to, że zmusza sklep do bycia singletonem . To bardzo utrudnia wdrożenie renderowania serwera . Na serwerze będziesz chciał, aby każde żądanie miało własny sklep, aby różni użytkownicy otrzymywali różne wstępnie załadowane dane.

Sklep singletonowy dodatkowo utrudnia testowanie. Nie można już kpić ze sklepu podczas testowania twórców akcji, ponieważ odnoszą się one do konkretnego prawdziwego sklepu wyeksportowanego z określonego modułu. Nie można nawet zresetować jego stanu z zewnątrz.

Więc chociaż technicznie możesz eksportować sklep singleton z modułu, odradzamy go. Nie rób tego, chyba że masz pewność, że Twoja aplikacja nigdy nie doda renderowania serwera.

Powrót do poprzedniej wersji:

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

To rozwiązuje problemy z powielaniem logiki i ratuje nas przed warunkami wyścigowymi.

Thunk Middleware

W przypadku prostych aplikacji podejście powinno wystarczyć. Nie martw się o oprogramowanie pośrednie, jeśli jesteś z niego zadowolony.

W większych aplikacjach mogą się jednak pojawić pewne niedogodności.

Na przykład wydaje się niefortunne, że musimy to omijać dispatch. Utrudnia to oddzielenie kontenera i komponentów prezentacji, ponieważ każdy komponent, który wywołuje akcje Redux asynchronicznie w powyższy sposób, musi zaakceptować dispatchjako rekwizyt, aby mógł go przekazać dalej. Nie można już po prostu wiązać twórców akcji, connect()ponieważ showNotificationWithTimeout()tak naprawdę nie jest twórcą akcji. Nie zwraca akcji Redux.

Ponadto może być niewygodne zapamiętanie, które funkcje są twórcami akcji synchronicznych, showNotification()a które asynchronicznymi pomocnikami showNotificationWithTimeout(). Musisz ich używać inaczej i uważaj, aby ich nie pomylić.

To była motywacja do znalezienia sposobu na „legitymizowanie” tego schematu zapewniania dispatchfunkcji pomocniczej i pomoc Redux „zobaczyć” takich twórców akcji asynchronicznych jako szczególny przypadek zwykłych twórców akcji, a nie całkowicie różnych funkcji.

Jeśli nadal jesteś z nami i rozpoznajesz problem w swojej aplikacji, możesz skorzystać z oprogramowania pośredniego Redux Thunk .

Krótko mówiąc, Redux Thunk uczy Redux rozpoznawania specjalnych rodzajów akcji, które w rzeczywistości działają:

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

Gdy to oprogramowanie pośrednie jest włączone, jeśli wyślesz funkcję , oprogramowanie pośrednie Redux Thunk poda ją dispatchjako argument. Będzie także „połykać” takie akcje, więc nie martw się, że twoje reduktory otrzymają dziwne argumenty funkcji. Twoje reduktory będą otrzymywać tylko proste działania na obiektach - albo emitowane bezpośrednio, albo emitowane przez funkcje, które właśnie opisaliśmy.

To nie wygląda bardzo przydatne, prawda? Nie w tej konkretnej sytuacji. Pozwala nam to jednak zadeklarować showNotificationWithTimeout()jako zwykłego twórcę akcji 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)
  }
}

Zwróć uwagę, że funkcja jest prawie identyczna z tą, którą napisaliśmy w poprzedniej sekcji. Jednak nie przyjmuje tego dispatchjako pierwszego argumentu. Zamiast tego zwraca funkcję, która przyjmuje dispatchjako pierwszy argument.

Jak wykorzystalibyśmy to w naszym komponencie? Zdecydowanie możemy napisać to:

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

Wzywamy twórcę działania asynchronicznego, aby uzyskać wewnętrzną funkcję, która chce właśnie dispatch, a następnie przechodzimy dispatch.

Jest to jednak jeszcze bardziej niezręczne niż oryginalna wersja! Dlaczego w ogóle poszliśmy tą drogą?

Z powodu tego, co mówiłem wcześniej. Jeśli oprogramowanie pośrednie Redux Thunk jest włączone, za każdym razem, gdy spróbujesz wywołać funkcję zamiast obiektu akcji, oprogramowanie pośrednie wywoła tę funkcję z dispatchsamą metodą jako pierwszy argument .

Zamiast tego możemy to zrobić:

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

Wreszcie, wywołanie akcji asynchronicznej (tak naprawdę serii akcji) nie różni się niczym od synchronizacji pojedynczej akcji z komponentem. Co jest dobre, ponieważ komponenty nie powinny dbać o to, czy coś dzieje się synchronicznie czy asynchronicznie. Właśnie to wyabstrahowaliśmy.

Zauważ, że skoro „nauczyliśmy” Reduxa rozpoznawania takich „specjalnych” twórców akcji (nazywamy ich twórcami akcji „ thunk” ), możemy teraz używać ich w dowolnym miejscu, w którym używalibyśmy zwykłych twórców akcji. Możemy na przykład używać ich z 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)

Czytanie stanu w kępach

Zazwyczaj reduktory zawierają logikę biznesową do określania następnego stanu. Reduktory uruchamiają się jednak dopiero po wysłaniu akcji. Co się stanie, jeśli masz efekt uboczny (taki jak wywołanie interfejsu API) w kreatorze akcji thunk i chcesz temu zapobiec pod pewnymi warunkami?

Bez użycia grubego oprogramowania pośredniego wystarczy wykonać następujące sprawdzenie w składniku:

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

Jednak celem wyodrębnienia twórcy akcji było scentralizowanie tej powtarzalnej logiki w wielu komponentach. Na szczęście Redux Thunk oferuje sposób na odczytanie aktualnego stanu sklepu Redux. Oprócz dispatchtego przekazuje również getStatejako drugi argument funkcji zwracanej przez twórcę akcji. Dzięki temu kolega może odczytać bieżący stan sklepu.

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

Nie nadużywaj tego wzoru. Jest dobry do ratowania wywołań API, gdy dostępne są buforowane dane, ale nie jest to bardzo dobra podstawa do budowania logiki biznesowej. Jeśli używasz getState()tylko warunkowo, aby wywołać różne działania, rozważ umieszczenie logiki biznesowej w reduktorach.

Następne kroki

Teraz, gdy masz podstawową intuicję na temat działania thunks, sprawdź przykład asynchroniczny Redux, który ich używa.

Możesz znaleźć wiele przykładów, w których thunks zwracają obietnice. Nie jest to wymagane, ale może być bardzo wygodne. Redux nie dba o to, co zwrócisz z thunk, ale daje ci wartość zwrotu dispatch(). Właśnie dlatego możesz zwrócić Obietnicę od dziadka i poczekać na jej wypełnienie, dzwoniąc dispatch(someThunkReturningPromise()).then(...).

Możesz także podzielić złożonych twórców akcji thunk na kilku mniejszych twórców akcji thunk. dispatchMetoda dostarczana przez łącznikami może zaakceptować łącznikami siebie, dzięki czemu można zastosować wzór rekurencyjnie. Ponownie działa to najlepiej w przypadku obietnic, ponieważ można zaimplementować asynchroniczny przepływ sterowania.

W przypadku niektórych aplikacji możesz znaleźć się w sytuacji, w której wymagania dotyczące asynchronicznego przepływu sterowania są zbyt złożone, aby można je było wyrazić z grubsza. Na przykład ponawianie nieudanych żądań, przepływ ponownej autoryzacji za pomocą tokenów lub wdrażanie krok po kroku może być zbyt szczegółowe i podatne na błędy, jeśli zostanie napisane w ten sposób. W takim przypadku warto przyjrzeć się bardziej zaawansowanym rozwiązaniom asynchronicznego sterowania przepływem, takim jak Redux Saga lub Redux Loop . Oceń je, porównaj przykłady odpowiadające Twoim potrzebom i wybierz ten, który najbardziej Ci się podoba.

Wreszcie, nie używaj niczego (w tym thunks), jeśli nie potrzebujesz ich naprawdę. Pamiętaj, że w zależności od wymagań Twoje rozwiązanie może wyglądać tak prosto, jak

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

Nie przejmuj się, chyba że wiesz, dlaczego to robisz.


27
Działania asynchroniczne wydają się prostym i eleganckim rozwiązaniem typowego problemu. Dlaczego ich obsługa nie jest wprowadzana w celu reduxingu bez potrzeby używania oprogramowania pośredniego? Ta odpowiedź mogłaby być o wiele bardziej zwięzła.
Phil Mander

83
@PhilMander Ponieważ istnieje wiele alternatywnych wzorów, takich jak github.com/raisemarketplace/redux-loop lub github.com/yelouafi/redux-saga, które są równie (jeśli nie bardziej) eleganckie. Redux to narzędzie niskiego poziomu. Możesz zbudować wybrany superset i rozdzielić go osobno.
Dan Abramov

16
Czy możesz to wyjaśnić: * rozważ umieszczenie logiki biznesowej w reduktorach *, czy to oznacza, że ​​powinienem wysłać akcję, a następnie określić w reduktorze, jakie dalsze akcje należy wysłać w zależności od mojego stanu? Moje pytanie brzmi: czy następnie wysyłam inne działania bezpośrednio do mojego reduktora, a jeśli nie, to skąd je wysyłam?
froginvasion

25
To zdanie ma zastosowanie wyłącznie do przypadku synchronicznego. Na przykład, jeśli piszesz, if (cond) dispatch({ type: 'A' }) else dispatch({ type: 'B' })może powinieneś po prostu dispatch({ type: 'C', something: cond })i zignorować akcję w reduktorach zamiast w zależności od action.somethingi bieżącego stanu.
Dan Abramov,

29
@ DanAbramov Masz moje poparcie dla tego „Jeśli nie masz tego problemu, skorzystaj z tego, co oferuje język i znajdź najprostsze rozwiązanie”. Dopiero wtedy zrozumiałem, kto to napisał!
Matt Lacey,

189

Korzystanie z sagi Redux

Jak powiedział Dan Abramov, jeśli chcesz bardziej zaawansowanej kontroli nad kodem asynchronicznym, możesz rzucić okiem na redux-saga .

Ta odpowiedź jest prostym przykładem. Jeśli chcesz uzyskać lepsze wyjaśnienia, dlaczego redux-saga może być przydatny w twojej aplikacji, sprawdź tę inną odpowiedź .

Ogólna idea jest taka, że ​​Redux-saga oferuje interpreter generatorów ES6, który pozwala łatwo pisać kod asynchroniczny, który wygląda jak kod synchroniczny (dlatego często znajdziesz skończoną pętlę w sadze Redux). W jakiś sposób Redux-saga buduje swój własny język bezpośrednio w Javascript. Na początku saga Redux może być nieco trudna do nauczenia, ponieważ potrzebujesz podstawowej wiedzy na temat generatorów, ale także rozumiesz język oferowany przez sagę Redux.

Spróbuję tutaj opisać tutaj system powiadomień, który zbudowałem na podstawie redux-sagi. Ten przykład obecnie działa w produkcji.

Zaawansowana specyfikacja systemu powiadomień

  • Możesz poprosić o wyświetlenie powiadomienia
  • Możesz poprosić o powiadomienie o ukrycie
  • Powiadomienie nie powinno być wyświetlane dłużej niż 4 sekundy
  • Jednocześnie może być wyświetlanych wiele powiadomień
  • Jednocześnie mogą być wyświetlane nie więcej niż 3 powiadomienia
  • Jeśli wymagane jest powiadomienie, gdy są już wyświetlane 3 powiadomienia, ustaw je w kolejce / odłóż.

Wynik

Zrzut ekranu mojej aplikacji produkcyjnej Stample.co

grzanki

Kod

Tutaj nazwałem powiadomienie, toastale jest to szczegół nazewnictwa.

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

I reduktor:

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

Stosowanie

Możesz po prostu wysłać TOAST_DISPLAY_REQUESTEDzdarzenia. Jeśli wyślesz 4 żądania, zostaną wyświetlone tylko 3 powiadomienia, a czwarte pojawi się nieco później, gdy zniknie pierwsze powiadomienie.

Pamiętaj, że nie polecam wysyłania TOAST_DISPLAY_REQUESTEDz JSX. Wolisz dodać kolejną sagę, która słucha już istniejących zdarzeń aplikacji, a następnie wysłać TOAST_DISPLAY_REQUESTED: Twój komponent, który uruchamia powiadomienie, nie musi być ściśle powiązany z systemem powiadomień.

Wniosek

Mój kod nie jest idealny, ale działa w produkcji z 0 błędami przez miesiące. Saga Redux i generatory są początkowo trochę trudne, ale kiedy je zrozumiesz, ten system jest dość łatwy do zbudowania.

Implementacja bardziej złożonych reguł, takich jak:

  • gdy zbyt wiele powiadomień jest „umieszczanych w kolejce”, daj mniej czasu wyświetlania dla każdego powiadomienia, aby rozmiar kolejki mógł się szybciej zmniejszyć.
  • wykryj zmiany rozmiaru okna i odpowiednio zmień maksymalną liczbę wyświetlanych powiadomień (na przykład pulpit = 3, portret telefonu = 2, krajobraz telefonu = 1)

Szczerze mówiąc, powodzenia we wdrażaniu tego rodzaju rzeczy poprawnie z thunkami.

Zauważ, że możesz zrobić dokładnie to samo z obserwowalnymi redux, które są bardzo podobne do sagi redux. Jest prawie taki sam i jest kwestią gustu między generatorami a RxJS.


18
Chciałbym, aby twoja odpowiedź nadeszła wcześniej, gdy pytanie zostało zadane, ponieważ nie mogę się bardziej zgodzić z używaniem biblioteki efektów ubocznych Sagi dla takiej logiki biznesowej. Reduktory i twórcy akcji są przeznaczone do zmian stanu. Przepływy pracy to nie to samo, co funkcje zmiany stanu. Przepływy pracy przechodzą przez przejścia, ale same nie są przejściami. Redux + React nie ma tego samodzielnie - właśnie dlatego Redux Saga jest tak przydatna.
Atticus

4
Dzięki, staram się, aby z tych powodów popularność redux-sagi stała się popularna :) zbyt mało osób uważa, że ​​redaga-saga jest obecnie tylko zamiennikiem dla thunksów i nie widzę, w jaki sposób redux-saga umożliwia złożone i oddzielone przepływy pracy
Sebastien Lorber

1
Dokładnie. Działania i redukcje są częścią automatu stanowego. Czasami, w przypadku złożonych przepływów pracy, potrzebujesz czegoś innego, aby zorganizować maszynę stanu, która nie jest bezpośrednio częścią samej maszyny stanu!
Atticus

2
Działania: ładunki / zdarzenia do stanu przejściowego. Reduktory: funkcje zmiany stanu. Komponenty: Interfejsy użytkownika odzwierciedlające stan. Ale brakuje jednego ważnego elementu - w jaki sposób zarządzasz procesem wielu przejść, które mają swoją własną logikę, która określa, które przejście należy wykonać w następnej kolejności? Redux Saga!
Atticus

2
@mrbrdo, jeśli uważnie przeczytasz moją odpowiedź, zauważysz, że limity czasu powiadomień są faktycznie obsługiwane yield call(delay,timeoutValue);: nie jest to ten sam interfejs API, ale ma ten sam efekt
Sebastien Lorber

25

Repozytorium z przykładowymi projektami

Obecnie istnieją cztery przykładowe projekty:

  1. Pisanie kodu asynchronicznego w linii
  2. Wyodrębnianie Async Action Creator
  3. Użyj Redux Thunk
  4. Saga Redux

Przyjęta odpowiedź jest niesamowita.

Ale czegoś brakuje:

  1. Nie można uruchomić przykładowych projektów, tylko niektóre fragmenty kodu.
  2. Brak przykładowego kodu dla innych alternatyw, takich jak:
    1. Saga Redux

Utworzyłem więc repozytorium Hello Async, aby dodać brakujące rzeczy:

  1. Projekty możliwe do uruchomienia. Możesz je pobrać i uruchomić bez modyfikacji.
  2. Podaj przykładowy kod, aby uzyskać więcej alternatyw:

Saga Redux

Akceptowana odpowiedź zawiera już przykładowe fragmenty kodu dla Async Code Inline, Async Action Generator i Redux Thunk. W celu zapewnienia kompletności udostępniam fragmenty kodu dla Sagi Redux:

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

Działania są proste i czyste.

// component.js

import { connect } from 'react-redux'

// ...

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

// ...

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

Nic nie jest wyjątkowe w przypadku komponentu.

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

Sagi oparte są na generatorach ES6

// index.js

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

const sagaMiddleware = createSagaMiddleware()

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

sagaMiddleware.run(saga)

W porównaniu do Redux Thunk

Plusy

  • Nie kończysz w piekle oddzwaniania.
  • Możesz łatwo przetestować swoje asynchroniczne przepływy.
  • Twoje działania pozostają czyste.

Cons

  • Zależy to od generatorów ES6, które są stosunkowo nowe.

Jeśli powyższe fragmenty kodu nie odpowiadają na wszystkie pytania, zapoznaj się z działającym projektem .


23

Możesz to zrobić za pomocą redux-thunk . Jest przewodnikiem Redux dokumencie działań asynchronicznych jak setTimeout.


Krótkie pytanie applyMiddleware(ReduxPromise, thunk)(createStore)uzupełniające , kiedy używasz oprogramowania pośredniego, czy w ten sposób dodajesz kilka oprogramowania pośredniego (oddzielone przecinkami?), Ponieważ nie wydaje mi się, żeby działało.
Ilja

1
@Ilja Powinno to działać:const store = createStore(reducer, applyMiddleware([ReduxPromise, thunk]));
geniuscarrier

22

Poleciłbym również przyjrzeć się wzorowi SAM .

Wzorzec SAM zaleca włączenie „predykatu następnego działania”, w którym (automatyczne) działania, takie jak „powiadomienia znikają automatycznie po 5 sekundach” są uruchamiane po aktualizacji modelu (model SAM ~ stan reduktora + sklep).

Wzorzec zaleca sekwencjonowanie akcji i mutacji modelu pojedynczo, ponieważ „stan kontrolny” modelu „kontroluje”, które akcje są włączone i / lub automatycznie wykonywane przez predykat następnej akcji. Po prostu nie można przewidzieć (ogólnie), w jakim stanie będzie system przed przetworzeniem akcji, a zatem czy następne oczekiwane działanie będzie dozwolone / możliwe.

Na przykład kod

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

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

nie byłoby dozwolone w SAM, ponieważ fakt, że można wywołać akcję hideNotification, zależy od tego, czy model pomyślnie zaakceptuje wartość „showNotication: true”. Mogą istnieć inne części modelu, które uniemożliwiają zaakceptowanie go, dlatego nie byłoby powodu, aby wywołać akcję hideNotification.

Zdecydowanie zaleciłbym wdrożenie poprawnego predykatu następnego działania po aktualizacji sklepu i poznaniu nowego stanu kontroli modelu. To najbezpieczniejszy sposób na wdrożenie pożądanego zachowania.

Możesz dołączyć do nas na Gitter, jeśli chcesz. Dostępny jest również przewodnik wprowadzający do SAM .


Do tej pory zarysowałem tylko powierzchnię, ale już jestem zachwycony wzorem SAM. V = S( vm( M.present( A(data) ) ), nap(M))jest po prostu piękna. Dziękujemy za podzielenie się swoimi przemyśleniami i doświadczeniem. Będę kopać głębiej.

@ ftor, dziękuję! kiedy napisałem to po raz pierwszy, miałem to samo uczucie. Używam SAM w produkcji już od prawie roku i nie mogę myśleć o czasach, w których czułem, że potrzebuję biblioteki do wdrożenia SAM (nawet vdom, chociaż widzę, kiedy można go użyć). Tylko jedna linia kodu, to wszystko! SAM produkuje kod izomorficzny, nie ma dwuznaczności, w jaki sposób radzić sobie z połączeniami asynchronicznymi ... Nie mogę myśleć o czasach, w których myślałem, co robię?
metaprogrammer

SAM to prawdziwy wzorzec inżynierii oprogramowania (właśnie stworzył z nim Alexa SDK). Opiera się na TLA + i stara się wnieść moc tej niesamowitej pracy do każdego programisty. SAM koryguje trzy aproksymacje, których (prawie) wszyscy używają od dziesięcioleci: - działania mogą manipulować stanem aplikacji - przypisania są równoważne mutacji - nie ma dokładnej definicji tego, co jest krokiem programowania (np. Krok = b * ca , czy 1 / odczytać b, c 2 / obliczyć b * c, 3 / przypisać z wynikiem trzy różne kroki?
metaprogrammer

20

Po wypróbowaniu różnych popularnych podejść (twórcy akcji, gromady, sagi, epopeje, efekty, niestandardowe oprogramowanie pośrednie) nadal czułem, że być może jest miejsce na ulepszenia, więc udokumentowałem swoją podróż w tym artykule na blogu, Gdzie mam umieścić logikę biznesową aplikacja React / Redux?

Podobnie jak w przypadku dyskusji tutaj, próbowałem skontrastować i porównać różne podejścia. W końcu doprowadziło mnie to do wprowadzenia nowej biblioteki redux-logiki, która czerpie inspirację z eposów, sag, niestandardowego oprogramowania pośredniego.

Pozwala przechwytywać działania w celu sprawdzania poprawności, weryfikacji, autoryzacji, a także zapewnia sposób wykonywania asynchronicznych operacji we / wy.

Niektóre typowe funkcje można po prostu zadeklarować, jak ogłaszanie, ograniczanie, anulowanie i tylko przy użyciu odpowiedzi z ostatniego żądania (takeLatest). redux-logic otacza kod, zapewniając tę ​​funkcjonalność dla Ciebie.

Dzięki temu możesz wdrożyć swoją podstawową logikę biznesową w dowolny sposób. Nie musisz używać obserwowalnych lub generatorów, chyba że chcesz. Używaj funkcji i wywołań zwrotnych, obietnic, funkcji asynchronicznych (asynchronizacja / oczekiwanie) itp.

Kod umożliwiający proste powiadomienie 5s wyglądałby mniej więcej tak:

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

W moim repozytorium mam bardziej zaawansowany przykład powiadomienia, który działa podobnie do opisanego przez Sebastiana Lorbera, w którym można ograniczyć wyświetlanie do N elementów i obracać je w kolejce. przykład powiadomienia redux-logic

Mam wiele przykładów live reds-logic jsfiddle, a także pełne przykłady . Nadal pracuję nad dokumentami i przykładami.

Chciałbym usłyszeć Twoją opinię.


Nie jestem pewien, czy podoba mi się twoja biblioteka, ale podoba mi się twój artykuł! Dobra robota, stary! Wykonałeś wystarczająco dużo pracy, aby zaoszczędzić czas innych.
Tyler Long,

2
Stworzyłem przykładowy projekt dla redux-logic tutaj: github.com/tylerlong/hello-async/tree/master/redux-logic Myślę, że to dobrze zaprojektowane oprogramowanie i nie widzę żadnych poważnych wad w porównaniu do innych alternatywy
Tyler Long

9

Rozumiem, że to pytanie jest trochę stare, ale zamierzam wprowadzić inne rozwiązanie, używając obserwowalnej redux aka. Epicki.

Cytując oficjalną dokumentację:

Co można zaobserwować przy reduxie?

Oprogramowanie pośrednie oparte na RxJS 5 dla Redux. Komponuj i anuluj działania asynchroniczne, aby tworzyć efekty uboczne i nie tylko.

Epos jest podstawowym prymitywem obserwowalnego redux.

Jest to funkcja, która pobiera strumień akcji i zwraca strumień akcji. Działania w, działania w górę.

Krótko mówiąc, możesz utworzyć funkcję, która odbiera akcje za pośrednictwem Strumienia, a następnie zwraca nowy strumień akcji (używając typowych efektów ubocznych, takich jak przekroczenia limitu czasu, opóźnienia, interwały i żądania).

Pozwól, że opublikuję kod, a następnie wyjaśnię go nieco więcej

store.js

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

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

  return state
}

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

  return outgoing;
}

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

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

index.js

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

const store = configureStore()

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

App.js

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

class App extends Component {

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

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

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

export default connect(mapStateToProps, mapDispatchToProps)(App)

Kluczowy kod do rozwiązania tego problemu jest tak łatwy, jak widać, jedyną rzeczą, która wydaje się inna niż inne odpowiedzi, jest funkcja rootEpic.

Punkt 1. Podobnie jak w przypadku sag, musisz połączyć epopeję, aby uzyskać funkcję najwyższego poziomu, która odbiera strumień akcji i zwraca strumień akcji, dzięki czemu można go używać z fabryką oprogramowania pośredniego createEpicMiddleware . W naszym przypadku potrzebujemy tylko jednego, więc mamy tylko nasz rootEpic, więc nie musimy niczego łączyć, ale dobrze jest wiedzieć.

Punkt 2. Nasza rootEpic, która dba o logikę efektów ubocznych, zajmuje tylko około 5 linii kodu, co jest niesamowite! W tym fakt, że jest to prawie deklaratywne!

Punkt 3. Pierwiastek po linijce Wyjaśnienie (w komentarzach)

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

Mam nadzieję, że to pomoże!


Czy możesz wyjaśnić, co robią tutaj konkretne metody API, na przykład switchMap?
Dmitri Zaitsev

1
Używamy obserwowalnych zmian w naszej aplikacji React Native na Windows. Jest to eleganckie rozwiązanie do implementacji złożonego, wysoce asynchronicznego problemu i ma fantastyczne wsparcie za pośrednictwem kanału Gitter i problemów GitHub. Dodatkowa warstwa złożoności jest tego warta tylko wtedy, gdy dojdziesz do konkretnego problemu, który ma oczywiście rozwiązać.
Matt Hargett,

8

Dlaczego to takie trudne? To tylko logika interfejsu użytkownika. Użyj dedykowanej akcji, aby ustawić dane powiadomień:

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

oraz dedykowany komponent do jego wyświetlenia:

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

W takim przypadku pytania powinny brzmieć „jak wyczyścić stary stan?”, „Jak powiadomić komponent o zmianie czasu”

Możesz zaimplementować akcję TIMEOUT, która jest wywoływana w setTimeout ze składnika.

Może wystarczy wyczyścić, gdy pojawi się nowe powiadomienie.

W każdym razie powinno setTimeoutgdzieś być , prawda? Dlaczego nie zrobić tego w komponencie

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

Motywacja polega na tym, że funkcja „zanikania powiadomień” jest naprawdę problemem dotyczącym interfejsu użytkownika. Upraszcza to testowanie logiki biznesowej.

Wydaje się, że nie ma sensu testować, jak jest zaimplementowany. Sensowne jest jedynie sprawdzenie, kiedy upłynie limit czasu powiadomienia. W ten sposób mniej kodu do kodu pośredniczącego, szybsze testy, czystszy kod.


1
To powinna być najlepsza odpowiedź.
mmla

6

Jeśli chcesz obsługiwać limity czasu dla akcji selektywnych, możesz wypróbować metodę oprogramowania pośredniego . Miałem podobny problem z selektywną obsługą akcji opartych na obietnicach, a to rozwiązanie było bardziej elastyczne.

Powiedzmy, że Twój twórca akcji wygląda następująco:

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

Limit czasu może zawierać wiele wartości w powyższej akcji

  • liczba w ms - na określony czas oczekiwania
  • true - przez stały czas oczekiwania. (obsługiwane w oprogramowaniu pośrednim)
  • nieokreślony - do natychmiastowej wysyłki

Implementacja oprogramowania pośredniego wygląda następująco:

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

Możesz teraz kierować wszystkie swoje działania przez tę warstwę oprogramowania pośredniego, używając funkcji redux.

createStore(reducer, applyMiddleware(timeoutMiddleware))

Możesz znaleźć podobne przykłady tutaj


5

Odpowiednim sposobem na to jest użycie Redux Thunk, popularnego oprogramowania pośredniego dla Redux, zgodnie z dokumentacją Redux Thunk:

„Oprogramowanie pośrednie Redux Thunk pozwala pisać twórców akcji, którzy zwracają funkcję zamiast akcji. Za pomocą thunk można opóźnić wysłanie akcji lub wysłać tylko wtedy, gdy spełniony jest określony warunek. Funkcja wewnętrzna odbiera metody sklepu dispatch i getState as parameters ".

Zasadniczo zwraca więc funkcję i można opóźnić wysyłkę lub ustawić ją w stanie warunkowym.

Więc coś takiego zrobi dla ciebie zadanie:

import ReduxThunk from 'redux-thunk';

const INCREMENT_COUNTER = 'INCREMENT_COUNTER';

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

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

4

To jest proste. Użyj pakietu trim-redux i napisz w ten componentDidMountlub inny sposób i zabij go componentWillUnmount.

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

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

3

Sam Redux jest dość obszerną biblioteką i do takich rzeczy musiałbyś użyć czegoś takiego jak Redux-thunk , który da dispatchfunkcję, dzięki czemu będziesz mógł wysłać zamknięcie powiadomienia po kilku sekundach.

Utworzyłem bibliotekę, aby rozwiązać takie problemy, jak gadatliwość i kompozycyjność, a twój przykład będzie wyglądał następująco:

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

Tworzymy więc akcje synchronizacji do wyświetlania powiadomień w akcji asynchronicznej, które mogą żądać informacji w tle lub sprawdzać później, czy powiadomienie zostało zamknięte ręcznie.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.