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_NOTIFICATION
upł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ć showNotificationWithTimeout
bez 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 dispatch
jako pierwszy argument? Ponieważ musi wysłać działania do sklepu. Zwykle komponent ma do niego dostęp, dispatch
ale 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 dispatch
bezpoś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ć dispatch
jako 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 dispatch
funkcji 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ą dispatch
jako 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 dispatch
jako pierwszego argumentu. Zamiast tego zwraca funkcję, która przyjmuje dispatch
jako 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 dispatch
samą 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 dispatch
tego przekazuje również getState
jako 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. dispatch
Metoda 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.
redux-saga
odpowiedzi, 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