根據文檔, 「沒有中間件,Redux存儲僅支持同步數據流」 。 我不明白爲何會這樣。 爲何容器組件不能調用異步API,而後dispatch
操做? html
例如,想象一個簡單的UI:一個字段和一個按鈕。 當用戶按下按鈕時,該字段將填充來自遠程服務器的數據。 前端
import * as React from 'react'; import * as Redux from 'redux'; import { Provider, connect } from 'react-redux'; const ActionTypes = { STARTED_UPDATING: 'STARTED_UPDATING', UPDATED: 'UPDATED' }; class AsyncApi { static getFieldValue() { const promise = new Promise((resolve) => { setTimeout(() => { resolve(Math.floor(Math.random() * 100)); }, 1000); }); return promise; } } class App extends React.Component { render() { return ( <div> <input value={this.props.field}/> <button disabled={this.props.isWaiting} onClick={this.props.update}>Fetch</button> {this.props.isWaiting && <div>Waiting...</div>} </div> ); } } App.propTypes = { dispatch: React.PropTypes.func, field: React.PropTypes.any, isWaiting: React.PropTypes.bool }; const reducer = (state = { field: 'No data', isWaiting: false }, action) => { switch (action.type) { case ActionTypes.STARTED_UPDATING: return { ...state, isWaiting: true }; case ActionTypes.UPDATED: return { ...state, isWaiting: false, field: action.payload }; default: return state; } }; const store = Redux.createStore(reducer); const ConnectedApp = connect( (state) => { return { ...state }; }, (dispatch) => { return { update: () => { dispatch({ type: ActionTypes.STARTED_UPDATING }); AsyncApi.getFieldValue() .then(result => dispatch({ type: ActionTypes.UPDATED, payload: result })); } }; })(App); export default class extends React.Component { render() { return <Provider store={store}><ConnectedApp/></Provider>; } }
渲染導出的組件後,我能夠單擊按鈕,而且輸入已正確更新。 react
注意connect
調用中的update
功能。 它調度一個動做,告訴應用程序正在更新,而後執行異步調用。 調用完成後,將提供的值做爲另外一個操做的有效負載進行分派。 git
這種方法有什麼問題? 如文檔所示,我爲何要使用Redux Thunk或Redux Promise? 程序員
編輯:我在Redux倉庫中搜索了線索,發現過去須要Action Creators是純函數。 例如, 如下用戶試圖爲異步數據流提供更好的解釋: es6
動做建立者自己仍然是一個純函數,可是它返回的thunk函數並不須要,它能夠執行異步調用 github
動做建立者再也不須要是純粹的。 所以,過去確定須要使用thunk / promise中間件,但彷佛再也不是這種狀況了? ajax
簡短的回答 :對我來講,這彷佛是一種徹底合理的解決異步問題的方法。 有幾個警告。 npm
在剛開始工做的新項目中,我有很是類似的思路。 我是Vanilla Redux優雅的系統的忠實粉絲,該系統用於更新商店和從新渲染組件,而這種方式不影響React組件樹。 我彷佛迷上了那種優雅的dispatch
機制來處理異步問題。 編程
最後,我採用了一種很是相似的方法,該方法與我從項目中剔除出來的庫中的庫相似,咱們稱之爲react-redux-controller 。
因爲如下幾個緣由,我最終沒有采用您所擁有的確切方法:
dispatch
。 一旦connect
語句失控,這將限制重構的選項-僅使用一種update
方法就顯得很是笨拙。 所以,若是須要將這些調度程序功能分解爲單獨的模塊,則須要一些系統來組成這些調度程序功能。 總而言之,您必須裝配一些系統以容許dispatch
以及將商店以及事件的參數注入到您的調度功能中。 我知道這種依賴注入的三種合理方法:
dispatch
中間件方法,可是我認爲它們基本相同。 connect
的第一個參數傳遞的功能,而沒必要直接與原始的規範化商店一塊兒使用。 this
上下文中,從而以面向對象的方式進行操做。 更新資料
在我看來,這個難題的一部分是react-redux的侷限性。 connect
的第一個參數獲取狀態快照,但不發送。 第二個參數獲取調度,但不獲取狀態。 因爲可以在繼續/回調時看到更新的狀態,所以兩個參數都不會關閉當前狀態。
要回答開始時提出的問題:
爲何容器組件不能調用異步API,而後分派操做?
請記住,這些文檔適用於Redux,而不適用於Redux plus React。 鏈接到React組件的 Redux存儲能夠徹底按照您所說的進行操做,可是沒有中間件的Plain Jane Redux存儲不接受除普通ol'對象以外的用於dispatch
參數。
沒有中間件,您固然仍然能夠作
const store = createStore(reducer); MyAPI.doThing().then(resp => store.dispatch(...));
但這是相似的狀況,其中異步被圍繞在 Redux上,而不是由 Redux處理。 所以,中間件經過修改能夠直接傳遞給dispatch
內容來實現異步。
也就是說,我認爲您的建議的精神是正確的。 固然,您還可使用其餘方法在Redux + React應用程序中處理異步。
使用中間件的好處之一是,您能夠繼續正常使用動做建立者,而沒必要擔憂它們是如何鏈接的。 例如,使用redux-thunk
,您編寫的代碼看起來很像
function updateThing() { return dispatch => { dispatch({ type: ActionTypes.STARTED_UPDATING }); AsyncApi.getFieldValue() .then(result => dispatch({ type: ActionTypes.UPDATED, payload: result })); } } const ConnectedApp = connect( (state) => { ...state }, { update: updateThing } )(App);
看起來與原始版本沒有什麼不一樣-只是改了一點-而且connect
不知道updateThing
是(或須要是)異步的。
若是你也想支持的承諾 , 觀測 , 傳奇 ,或瘋狂的定製和高度聲明動做的創造者,那麼終極版能夠只經過改變你傳遞什麼作dispatch
(又名,你的行動創造者返回的內容)。 無需對React組件(或connect
調用)進行處理。
這種方法有什麼問題? 如文檔所示,我爲何要使用Redux Thunk或Redux Promise?
這種方法沒有錯。 這在大型應用程序中很不方便,由於您將有不一樣的組件執行相同的操做,您可能但願對某些操做進行反跳操做,或者將某些本地狀態(例如,自動遞增ID)保持在操做建立者附近,等等。從維護角度將動做建立者提取到單獨的功能中。
您能夠閱讀我對「如何在超時時分派Redux操做」的回答,以獲取更詳細的演練。
中間件像終極版咚或終極版無極只是給你「語法糖」派遣的thunk或承諾,但你沒必要使用它。
所以,沒有任何中間件,您的動做建立者可能看起來像
// action creator function loadData(dispatch, userId) { // needs to dispatch, so it is first argument return fetch(`http://data.com/${userId}`) .then(res => res.json()) .then( data => dispatch({ type: 'LOAD_DATA_SUCCESS', data }), err => dispatch({ type: 'LOAD_DATA_FAILURE', err }) ); } // component componentWillMount() { loadData(this.props.dispatch, this.props.userId); // don't forget to pass dispatch }
可是,使用Thunk中間件,您能夠這樣編寫:
// action creator function loadData(userId) { return dispatch => fetch(`http://data.com/${userId}`) // Redux Thunk handles these .then(res => res.json()) .then( data => dispatch({ type: 'LOAD_DATA_SUCCESS', data }), err => dispatch({ type: 'LOAD_DATA_FAILURE', err }) ); } // component componentWillMount() { this.props.dispatch(loadData(this.props.userId)); // dispatch like you usually do }
因此沒有太大的區別。 我喜歡後一種方法的一件事是,該組件不在意動做建立者是異步的。 它只是正常地調用dispatch
,也可使用mapDispatchToProps
用mapDispatchToProps
法綁定此類操做建立者,等等。這些組件不知道操做建立者的實現方式,所以您能夠在不一樣的異步方法之間進行切換(Redux Thunk,Redux Promise, Redux Saga)而無需更改組件。 另外一方面,使用前一種顯式方法,您的組件能夠確切地知道特定的調用是異步的,而且須要經過某種約定來傳遞dispatch
(例如,做爲sync參數)。
還考慮一下此代碼將如何更改。 假設咱們要具備第二個數據加載功能,並將它們組合在一個動做建立器中。
使用第一種方法時,咱們須要注意咱們正在調用哪一種動做建立者:
// action creators function loadSomeData(dispatch, userId) { return fetch(`http://data.com/${userId}`) .then(res => res.json()) .then( data => dispatch({ type: 'LOAD_SOME_DATA_SUCCESS', data }), err => dispatch({ type: 'LOAD_SOME_DATA_FAILURE', err }) ); } function loadOtherData(dispatch, userId) { return fetch(`http://data.com/${userId}`) .then(res => res.json()) .then( data => dispatch({ type: 'LOAD_OTHER_DATA_SUCCESS', data }), err => dispatch({ type: 'LOAD_OTHER_DATA_FAILURE', err }) ); } function loadAllData(dispatch, userId) { return Promise.all( loadSomeData(dispatch, userId), // pass dispatch first: it's async loadOtherData(dispatch, userId) // pass dispatch first: it's async ); } // component componentWillMount() { loadAllData(this.props.dispatch, this.props.userId); // pass dispatch first }
使用Redux Thunk動做建立者能夠dispatch
其餘動做建立者的結果,甚至不考慮它們是同步的仍是異步的:
// action creators function loadSomeData(userId) { return dispatch => fetch(`http://data.com/${userId}`) .then(res => res.json()) .then( data => dispatch({ type: 'LOAD_SOME_DATA_SUCCESS', data }), err => dispatch({ type: 'LOAD_SOME_DATA_FAILURE', err }) ); } function loadOtherData(userId) { return dispatch => fetch(`http://data.com/${userId}`) .then(res => res.json()) .then( data => dispatch({ type: 'LOAD_OTHER_DATA_SUCCESS', data }), err => dispatch({ type: 'LOAD_OTHER_DATA_FAILURE', err }) ); } function loadAllData(userId) { return dispatch => Promise.all( dispatch(loadSomeData(userId)), // just dispatch normally! dispatch(loadOtherData(userId)) // just dispatch normally! ); } // component componentWillMount() { this.props.dispatch(loadAllData(this.props.userId)); // just dispatch normally! }
使用這種方法,若是之後您但願動做建立者查看當前的Redux狀態,則可使用傳遞給thunk的第二個getState
參數,而無需徹底修改調用代碼:
function loadSomeData(userId) { // Thanks to Redux Thunk I can use getState() here without changing callers return (dispatch, getState) => { if (getState().data[userId].isLoaded) { return Promise.resolve(); } fetch(`http://data.com/${userId}`) .then(res => res.json()) .then( data => dispatch({ type: 'LOAD_SOME_DATA_SUCCESS', data }), err => dispatch({ type: 'LOAD_SOME_DATA_FAILURE', err }) ); } }
若是須要將其更改成同步,則也能夠在不更改任何調用代碼的狀況下執行此操做:
// I can change it to be a regular action creator without touching callers function loadSomeData(userId) { return { type: 'LOAD_SOME_DATA_SUCCESS', data: localStorage.getItem('my-data') } }
所以,使用像Redux Thunk或Redux Promise這樣的中間件的好處是組件不知道動做建立者的實現方式,它們是否關心Redux狀態,它們是同步仍是異步以及是否調用其餘動做建立者。 缺點是間接的,但咱們認爲在實際應用中值得這麼作。
最後,Redux Thunk和朋友只是Redux應用程序中異步請求的一種可能方法。 另外一個有趣的方法是Redux Saga ,它容許您定義長時間運行的守護程序(「 sagas」),這些守護程序會在操做到來時執行操做,並在輸出操做以前轉換或執行請求。 這將邏輯從動做建立者轉移到了傳奇。 您可能想要檢查一下,而後選擇最適合您的東西。
我在Redux存儲庫中搜索了線索,發現過去過去要求Action Creators是純函數。
這是不正確的。 文檔說了這一點,可是文檔是錯誤的。
從未要求動做建立者是純粹的功能。
咱們修復了文檔以反映這一點。
丹·阿布拉莫夫(Dan Abramov)的答案是關於redux-thunk
可是我將進一步討論redux-saga ,它很是類似,但功能更強大。
redux-thunk
是命令性的/ redux-saga
是聲明性的 當您手頭有重擊時,例如IO monad或諾言,執行後就不會輕易知道它會作什麼。 測試一個thunk的惟一方法是執行它,並模擬調度程序(若是它與更多的東西交互,則模擬整個外部世界……)。
若是您正在使用模擬,那麼您就不在進行函數式編程。
從反作用的角度來看,模擬標誌着您的代碼是不純的,而且在功能程序員的眼中,它證實了某些錯誤。 與其下載圖書館來幫助咱們檢查冰山是否無缺,不如咱們繞着它航行。 一個TDD / Java頑固的傢伙曾經問我如何在Clojure中進行模擬。 答案是,咱們一般不這樣作。 咱們一般將其視爲咱們須要重構代碼的標誌。
sagas(由於它們在redux-saga
)是聲明性的,而且像Free monad或React組件同樣,它們無需任何模擬就易於測試。
另請參閱本文 :
在現代FP中,咱們不該該編寫程序,而應該編寫程序的描述,而後能夠對其進行自省,轉換和解釋。
(實際上,Redux-saga就像是一個混合體:流程是必須的,但效果是聲明性的)
前端世界對如何將某些後端概念(如CQRS / EventSourcing和Flux / Redux)相關聯感到困惑,主要是由於在Flux中,咱們使用術語「操做」來表示命令性代碼( LOAD_USER
)和事件( USER_LOADED
)。 我相信,像事件來源同樣,您應該只調度事件。
想象一個帶有用戶配置文件連接的應用程序。 使用兩種中間件來處理此問題的慣用方式是:
redux-thunk
<div onClick={e => dispatch(actions.loadUserProfile(123)}>Robert</div> function loadUserProfile(userId) { return dispatch => fetch(`http://data.com/${userId}`) .then(res => res.json()) .then( data => dispatch({ type: 'USER_PROFILE_LOADED', data }), err => dispatch({ type: 'USER_PROFILE_LOAD_FAILED', err }) ); }
redux-saga
<div onClick={e => dispatch({ type: 'USER_NAME_CLICKED', payload: 123 })}>Robert</div> function* loadUserProfileOnNameClick() { yield* takeLatest("USER_NAME_CLICKED", fetchUser); } function* fetchUser(action) { try { const userProfile = yield fetch(`http://data.com/${action.payload.userId }`) yield put({ type: 'USER_PROFILE_LOADED', userProfile }) } catch(err) { yield put({ type: 'USER_PROFILE_LOAD_FAILED', err }) } }
這個傳奇轉化爲:
每次單擊用戶名時,獲取用戶配置文件,而後使用加載的配置文件調度事件。
如您所見, redux-saga
有一些優勢。
使用takeLatest
能夠表示您只對獲取最後一次單擊的用戶名的數據感興趣(若是用戶在許多用戶名上快速單擊,則能夠處理併發問題)。 這樣的東西很難與暴徒。 若是您不但願這種行爲,可使用takeEvery
。
您可使動做創做者保持純正。 請注意,保留actionCreators(在sagas put
和component dispatch
)仍然頗有用,由於它可能會幫助您未來添加動做驗證(斷言/流程/打字稿)。
因爲效果是聲明性的,所以您的代碼變得更具可測試性
您再也不須要觸發相似rpc的調用,例如actions.loadUser()
。 您的UI只需調度已發生的事件。 咱們僅觸發事件 (始終以過去時!),再也不執行任何操做。 這意味着您能夠建立解耦的「鴨子」或有界上下文 ,而且傳奇能夠充當這些模塊化組件之間的耦合點。
這意味着您的視圖更易於管理,由於它們再也不須要在已發生的事情和應發生的事情之間包含轉換層
例如,想象一個無限滾動視圖。 CONTAINER_SCROLLED
能夠致使NEXT_PAGE_LOADED
,可是可滾動容器是否真的有責任決定咱們是否應該加載另外一頁? 而後,他必須知道更復雜的內容,例如是否成功加載了最後一頁,或者是否已有試圖加載的頁面,或者是否還有其餘要加載的項目? 我不這麼認爲:爲了得到最大的可重用性,可滾動容器應該只描述它已經被滾動。 頁面的加載是該滾動的「業務影響」
有人可能會爭辯說,生成器可使用本地變量將狀態固有地隱藏在redux存儲以外,可是若是您經過啓動計時器等開始在thunk中編排複雜的東西,則不管如何都會遇到一樣的問題。 如今有一個select
效果,能夠從Redux存儲中獲取一些狀態。
Sagas能夠進行時間旅行,還能夠啓用當前正在使用的複雜流記錄和開發工具。 這是一些已經實現的簡單異步流日誌記錄:
Sagas不只在替換redux thunk。 它們來自後端/分佈式系統/事件源。
一個廣泛的誤解是,sagas即將到來,以更好的可測試性取代您的redux thunk。 實際上,這只是redux-saga的實現細節。 使用聲明性效果比使用thunk更好,可是可在命令性或聲明性代碼之上實現saga模式。
首先,傳奇是一款軟件,能夠協調長期運行的事務(最終的一致性)以及跨不一樣有界上下文的事務(域驅動的設計術語)。
爲了簡化前端世界的操做,假設有widget1和widget2。 單擊widget1上的某個按鈕時,它將對widget2產生影響。 與其將2個小部件耦合在一塊兒(即,小部件1調度以小部件2爲目標的動做),小部件1僅調度其按鈕被單擊。 而後,傳奇偵聽此按鈕的單擊,而後經過調度widget2知道的新事件來更新widget2。
這增長了簡單應用程序沒必要要的間接級別,但使擴展複雜應用程序更加容易。 如今,您能夠將widget1和widget2發佈到不一樣的npm存儲庫,這樣它們就沒必要彼此瞭解,而沒必要共享全局的動做註冊表。 如今這2個小部件是能夠單獨使用的有界上下文。 他們不須要彼此保持一致,也能夠在其餘應用程序中重複使用。 傳奇是兩個小部件之間的耦合點,以對您的業務有意義的方式協調它們。
關於如何構建Redux應用的一些不錯的文章,出於解耦的緣由,您能夠在其中使用Redux-saga:
我但願個人組件可以觸發應用內通知的顯示。 可是我不但願個人組件與具備本身的業務規則的通知系統高度耦合(最多同時顯示3條通知,通知隊列,4秒顯示時間等)。
我不但願個人JSX組件決定什麼時候顯示/隱藏通知。 我只是給它請求通知的功能,而將複雜的規則留在了傳奇中。 這種東西很難經過大塊頭或諾言實現。
我在這裏描述了佐賀如何作到這一點
saga一詞來自後端世界。 在最初的長時間討論中 ,我最初將Yassine(Redux-saga的做者)介紹給該術語。
最初,該術語是在論文中引入的,該傳奇模式原本應該用於處理分佈式事務中的最終一致性,可是後端開發人員已將其用法擴展到更普遍的定義,所以如今它也涵蓋了「流程管理器」模式(某種程度上,原始的傳奇模式是流程管理器的一種特殊形式)。
今天,「傳奇」一詞使人困惑,由於它能夠描述兩種不一樣的事物。 因爲它在redux-saga中使用,所以它並不描述處理分佈式事務的方法,而是協調應用程序中的操做的方法。 redux-saga
也能夠稱爲redux-process-manager
。
也能夠看看:
若是您不喜歡使用生成器的想法,可是對saga模式及其解耦屬性感興趣,則還可使用redux-observable實現相同的功能,它使用名稱epic
來描述徹底相同的模式,但使用RxJS。 若是您已經熟悉Rx,就會有賓至如歸的感受。
const loadUserProfileOnNameClickEpic = action$ => action$.ofType('USER_NAME_CLICKED') .switchMap(action => Observable.ajax(`http://data.com/${action.payload.userId}`) .map(userProfile => ({ type: 'USER_PROFILE_LOADED', userProfile })) .catch(err => Observable.of({ type: 'USER_PROFILE_LOAD_FAILED', err })) );
yield put(someActionThunk)
調度yield put(someActionThunk)
。 若是您對使用Redux-saga(或Redux-observable)感到恐懼,但只須要使用去耦模式,請檢查redux-dispatch-subscribe :它容許偵聽調度並在偵聽器中觸發新的調度。
const unsubscribe = store.addDispatchListener(action => { if (action.type === 'ping') { store.dispatch({ type: 'pong' }); } });
Abramov的目標-也是每一個人的理想-只是在最合適的地方封裝複雜性(和異步調用) 。
在標準Redux數據流中最好的位置是哪裏? 怎麼樣: