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
let cache = createCache()
資源請求示例:
import { createResource } from react-cache
let InvoiceResource = createResource(
(id) => {
return fetch(`/invoices/${id}`).then(response => {
return response.json()
})
}
)
它僅需要接受一個返回 promise 的函數。如下是一個使用了上方的資源請求和緩存的組件示例:
import cache from ./cache
import InvoiceResource from ./InvoiceResource
let Invoice = ({ invoiceId }) => {
let invoice = 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 reducer
import { createStore } from redux
import { connect } from react-redux
let reducer = (state, action) => {
if (action.type === LOADED_INVOICE) {
return {
...state,
invoice: action.data
}
}
return state
}
let store = createStore(reducer)
/////////////////////////////////////////////
// the action
function fetchInvoice(dispatch, id) {
fetch(`/invoices/${id}`).then(response => {
dispatch({
type: LOADED_INVOICE,
data: response.json()
})
})
}
/////////////////////////////////////////////
// the component, all connected up
class Invoice extends React.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) {
return null
}
return <h1>{invoice.number}</h1>
}
}
export default connect(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-cacheAPI 極不穩定,最新發布的2.0.0-alpha.0API 已改,請慎重使用??),完整示例代碼可參考 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/suspense-sample/tree/react-cache-2
推薦閱讀:
