The Suspense is Killing Redux
譯文已獲得原作者授權,轉載時請附上原文鏈接medium.com/@ryanflorence/the-suspense-is-killing-redux-e888f9692430
在最近舉辦的workshop上我一直考慮這個問題:
Suspense 會殺死Redux 嗎?
不得不說上面的表達方式過於粗魯,但我認為Suspense 即將取代Redux。
React Suspense
主要用來處理異步數據請求時的頁面渲染,這方面是Redux
未曾涉及的,但為了做到這一點, Suspense
需要去接管客戶端的數據邏輯,而這也正是Redux
一直以來所擅長的。
如果你還沒有聽說過React Suspense
,可以先花點時間聽下Dan Abramov在JSConf冰島上的關於React Suspense的分享。
Suspense 的基本用法
使用Suspense
分成三個部分:緩存,資源請求和組件。
緩存範例:
import{createCache}from'react-cache'letcache=createCache()
資源請求範例:
import{createResource}from'react-cache'letInvoiceResource=createResource((id)=>{returnfetch(`/invoices/${id}`).then(response=>{returnresponse.json()})})
它僅需要接受一個返回promise 的函數。如下是一個使用了上方的資源請求和緩存的組件範例:
importcachefrom'./cache'importInvoiceResourcefrom'./InvoiceResource'letInvoice=({invoiceId})=>{letinvoice=InvoiceResource.read(cache,invoiceId)return(<h1>{invoice.number}</h1>)}
瞧,這就是Suspense
這個應用的渲染過程的步驟如下:
- React 開始渲染(在內存中)。
- 調用
InvoicesResource.read()
。 - invoiceId對應的緩存數據不存在,從而觸發
createResource
函數的調用,發送異步請求去獲取數據。 - 然後,緩存會throw第3步中返回的promise。 (這裡能throw的除了異常,也可以是其他東西,甚至可以是
window
對象)這個promise被React捕獲之後,會暫停渲染過程。 - React 等待異步請求結果(等待promise resolve)。
- 異步請求完成(promise resolve)。
- React嘗試再次渲染
Invoices
(在內存中)。 - 再次調用
InvoicesResource.read()
。 - 此時已有數據緩存,
ApiResource.read()
同步返回數據。 - React 渲染出頁面。
上方的步驟中,我們將異步的資源請求邏輯當做同步函數調用,用起來非常炫酷。
而關於資源請求時如何處理佔位符和Spinner,就不在這裡展開討論,我們只需要知道:React 將會維持在老的頁面,直到新的數據加載完成,當資源請求時間超時,可以先去渲染佔位符。
那麼在Redux 中又是如何工作呢?
在redux 中執行相同的流程的話,它會是這樣的:
/////////////////////////////////////////////// the store and reducerimport{createStore}from'redux'import{connect}from'react-redux'letreducer=(state,action)=>{if(action.type==='LOADED_INVOICE'){return{...state,invoice:action.data}}returnstate}letstore=createStore(reducer)/////////////////////////////////////////////// the actionfunctionfetchInvoice(dispatch,id){fetch(`/invoices/${id}`).then(response=>{dispatch({type:'LOADED_INVOICE',data:response.json()})})}/////////////////////////////////////////////// the component, all connected upclassInvoiceextendsReact.Component{componentDidMount(){fetchInvoice(this.props.dispatch,this.props.invoiceId)}componentDidUpdate(prevProps){if(prevProps.invoiceId!==this.props.invoiceId){fetchInvoice(this.props.dispatch,this.props.invoiceId)}}render(){if(!this.props.invoice){returnnull}return<h1>{invoice.number}</h1>}}exportdefaultconnect(state=>{return{invoices:state.invoices}})(Invoices)
對比一下
它們的工作方式不同,但也有些部分是相同的:
- store → cache
- mapStateToProps → Resource.read()
- action → function passed to resource
reducer 和dispatch 函數不復存在,因為已不再需要通過action 來更新state,而是直接從資源請求中讀取數據。當(緩存中)沒有數據時,會暫停渲染並等待數據請求返回。在某種程度上,cache + resources 同時充當了dispatch,reducers 和actions 的作用。
另外使用Suspense
時不需要使用生命週期hook函數。當invoiceId
改變而觸發Invoice
重新渲染時,若該invoiceId對應的緩存數據為空,則會自動發送新請求去獲取數據,這比在生命週期hook函數中監聽props的變化再dispatch一個action要簡單得多。
依我看來,無論你之前是習慣使用Redux store還是組件層的local state,切換到Suspense
都會毫無壓力。
因此,如果你使用Redux 主要用來獲取服務端數據並渲染的話,那麼Suspense 完全可以替換掉Redux。我會用Suspense,因為它能讓代碼更簡單,更好處理加載中的狀態。
緩存失效怎麼辦
createCache
的第一個參數就是一個用於處理失效的函數。我測試下來發現,一旦緩存數據更新,頁面就能根據新的數據直接重新渲染。這能客戶端數據緩存和服務器數據保持同步,看起來是不是很酷。
此外, Suspense
處理頁面更新的方式感覺很棒:新頁面在內存中渲染時,舊頁面仍然存在並且是可交互的,當數據更新後,頁面將使用新數據(來自服務端)重新渲染,要知道通常服務端渲染也是這樣工作的,這種方式也會讓同步客戶端和服務器數據的中間層的存在失去意義。
Suspense
無法取代一切
有些人正在使用Redux做更複雜的事情(比如將狀態保持與API同步的同時也存到localStorage中), Suspense
也不能取代Redux的所有應用場景。
但是,依我看來,我所接觸的大多數正在使用Redux的人,僅僅是用來獲取服務器端的資源數據,如果你也是其中一員的話,我想你會愛上Suspense
的易用性和用戶體驗。
參加workshop
本文作者Ryan Florence正在美國地區舉辦React相關workshop, 點擊查看workshop城市和日期
譯者後記
本文中提到的例子,目前已可以使用最新發布的react v16.6
版本體驗, react-cache
使用canary版本( react-cache
API極不穩定,最新發布的2.0.0-alpha.0
API已改,請慎重使用),完整範例代碼可參考foryuki/suspense-sample
更新
react-cache 2.0.0-alpha
版本中移除了createCache
方法,API從原來的Resource.read(cache, key)
變成Resource.read(key)
,內置了cache。詳請見issue:react-cache Remove cache as argument to read 。完整範例代碼可參考: https:// github.com/foryuki/susp ense-sample/tree/react-cache-2