在 Hooks 以前,若是須要在組件中執行異步任務,例如數據的增刪改查,咱們只能使用 class 組件,這是由於,一方面,咱們須要狀態來存儲任務的加載情況、錯誤以及數據,另外一方面,咱們也須要生命週期來調度任務:html
class List extends React.Component {
state = {
loading: false,
error: null,
data: [],
page: 1,
pageSize: 10
}
componentDidMount() {
fetch()
}
componentDidUpdate(prevProps, prevState) {
const { page, pageSize } = this.state
if (prevState.page != page || prevState.pageSize != pageSize) {
fetch()
}
}
handlePaginationChange = (page, pageSize) => {
this.setState({
page,
pageSize
})
}
fetch = () => {
// 設置加載態,重置錯誤
this.setState({
loading: true,
error: null
})
API.fetchList({
page: this.state.page,
pageSize: this.state.pageSize
}).then(data => {
this.setState({
loading: false,
data
})
}).catch(error => {
this.setState({
loading: false,
error,
data: []
})
})
}
render() {
const { page, pageSize, loading, data, error } = this.state
return error
? <Error error={error}>
: (
<>
<Table
data={this.state.data}
loading={loading}
/>
<Pagination
page={page}
pageSize={pageSize}
onChange={this.handlePaginationChange}
/>
</>
)
}
}
複製代碼
上例的列表組件爲咱們展現了,基於 Class 建立一個異步組件,咱們須要:react
componentDidUpdate
中的 if 分支)咱們知道,當 React 推出了 Hooks 後,爲本來單薄的函數組件帶來了:git
useState
hookuseEffect
hook這兩個能力可以讓函數組件像類組件同樣,建立異步任務,維護對應的數據狀態:sql
const List = props => {
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
const [data, setData] = useState(null)
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = uesState(10)
useEffect(() => {
setLoading(true)
setError(null)
API.fetchList({
page,
pageSize
}).then(data => {
setLoading(false)
setData(data)
}).catch(error => {
setLoading(false)
setError(error)
})
}, [page, pageSize])
const handlePaginationChange = useCallback((page, pageSize) => {
setPage(page)
setPageSize(pageSize)
}, [])
return error
? <Error error={error}>
: (
<>
<Table
data={data}
loading={loading}
/>
<Pagination
page={page}
pageSize={pageSize}
onChange={handlePaginationChange}
/>
</>
)
}
複製代碼
使用 hooks 與函數組件來建立異步組件時,咱們的關注的是:編程
useEffect
的第一個參數useEffect
的第二個參數上例中,異步反作用即
API.fetchList
,這個反作用依賴了兩個參數:page
及pageSize
。api
乍看之下,貌似咱們仍在用 「狀態 + 反作用」 的方式編排異步組件,但如今:數組
useEffect
足夠這樣咱們可以用 React 進一步的實踐函數響應式編程(FRP)。相似的模式並不新鮮,幾年前 Cycle.js 就已經這麼作了:app
import {run} from '@cycle/run'
import {div, label, input, hr, h1, makeDOMDriver} from '@cycle/dom'
function main(sources) {
const input$ = sources.DOM.select('.field').events('input')
const name$ = input$.map(ev => ev.target.value).startWith('')
const vdom$ = name$.map(name =>
div([
label('Name:'),
input('.field', {attrs: {type: 'text'}}),
hr(),
h1('Hello ' + name),
])
)
return { DOM: vdom$ }
}
run(main, { DOM: makeDOMDriver('#app-container') })
複製代碼
不一樣的是,Cycle.js 偏心 Hyperscript,React 則是 JSX,也沒有使用 FRP 框架或者 Observable(RxJS 或者 xstream)去組織依賴。框架
useService
:響應式服務調度上文中,使用了 hooks 和函數組件來建立異步組件,相較於基於 class 建立的異步組件,useEffect
砍掉了生命期,也砍掉了生命期內的命令式地調度粒度控制,代碼着實精簡很多。可是這還不夠,咱們仍能觀察到一些樣板代碼:dom
useEffect
中顯式地聲明它們在繼續精簡代碼以前,咱們還須要明確一點:Hooks 的到來,並不僅是爲函數組件帶來了狀態管理及反作用治理的能力,咱們使用 Hooks,也不僅是去重複 class 組件能作的事兒。它的到來,更帶來了獨立於 HOC 和 render props 以外的是新的邏輯複用方式,咱們能夠將其概括爲:
即,配置了一個 Hook 以後,若聲明瞭依賴,則每當依賴變更,將得到新的數據。基於此,咱們就能夠歸納出異步任務的對應的 Hook:
即,咱們定義了異步任務,並聲明其依賴爲請求參數,那麼,useService
hook 將爲咱們返回任務的加載情況、報錯以及數據,另外,當任意接口參數變更時,服務也會被自動調度。
如今,在組件中,一個 hook 就能搞定異步任務:
const List = props => {
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = uesState(10)
const { loading, error, response } = useService(API.fetchList, {page, pageSize})
const handlePaginationChange = useCallback((page, pageSize) => {
setPage(page)
setPageSize(pageSize)
}, [])
return error
? <Error error={error} />
: (
<>
<Table
data={this.state.data}
loading={loading}
/>
<Pagination
page={page}
pageSize={pageSize}
onChange={handlePaginationChange}
/>
</>
)
}
複製代碼
useService
的實現大體以下,要留意的是,在此實現中,咱們對前後兩次的參數進行了深度比較,保證只有在參數變更時,請求才會被髮出:
import { isEqual } from 'lodash'
function useService(service, params) {
const prevParams = useRef(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
const [response, setResponse] = useState(null)
useEffect(() => {
if (!isEqual(prevParams.current, params)) {
prevParams.current = params
setLoading(true)
setError(null)
service(params)
.then(response => {
setLoading(false)
setResponse(response)
})
.catch(error => {
setLoading(false)
setError(error)
})
}
})
return { loading, error, response }
}
複製代碼
這個例子你能夠在 CodeSandbox 查看,嘗試切換頁碼,或者頁面大小,你能看到請求被髮出,而切換表格尺寸時,由於參數沒有發生變化,請求將不會發出,即 effect 不會被執行。
誠然,咱們也可使用 HOC 或者 render props 實現上面的複用,好比下面展現的這樣,但它們在層次上容易造成組件嵌套,不如 Hooks + 函數組件那樣簡潔直接:
class List extends React.Component {
// ...
render() {
const { page, pageSize } = this.state
return (
<Service params={{page, pageSize}} service={fetchList}>
{({loading, error, response}) => error
? <Error error={error} />
: (
<>
<Table
data={response}
loading={loading}
/>
<Pagination
page={page}
pageSize={pageSize}
onChange={this.handlePaginationChange}
/>
</>
)}
</Service>
)
}
}
複製代碼
useServiceCallback
:手動服務調度有些時候,咱們也須要手動控制一個服務的調用,例如建立、刪除等操做,對於這樣的需求,咱們能夠再封裝一個 useServiceCallback
hook,除了 loading、error、response,它還可以返回服務調用函數。
下例中,每當咱們在 Auto Complete 組件中鍵入內容,都手動調用下搜索服務進行搜索:
const Search = props => {
const [api, { loading, error, response }] = useServiceCallback(search);
const handleSearch = useCallback(
value => {
if (value.length) {
api({
text: value
});
}
},
[api]
);
return (
<AutoComplete dataSource={response || []} onSearch={handleSearch} placeholder="等待輸入..." /> ); }; 複製代碼
它的實現你能夠在 CodeSandbox 上查看,而且咱們還用 useServiceCallback
實現了 useService
。
在實際項目中,咱們對於某個服務,可能還有這些訴求:
想要讓咱們的 useService
優雅地實現這些能力,就不得不搬出 RxJS 了,它能讓咱們聲明式地編排異步流程。在 Hooks 推出後,咱們也能更加天然的將 RxJS 能力注入到 Hooks 中,實現 RxJS 與 React 組件的解耦。
如何使用 RxJS 豐富咱們 useService
的 hook 能力就再也不本文贅述了,對此感興趣的同窗,能夠在 CodeSandbox 上看到實現和範例。如今,咱們的 service hook 用起來仍然簡單,可是功能更增強大:
const List = props => {
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = uesState(10)
const { loading, error, response } = useAdvancedService({
service: API.fetchList,
/** 加載延遲 */
loadingDelay: 500,
/** 重試次數 */
retry: 3,
/** 每次重試延遲 */
retryDelay: 500,
/** 競態處理策略 */
race: 'switch',
/** 成功回調 */
onSuccess: (resp) => {
console.log('Fetch success', resp)
},
/** 失敗回調 */
onError: (error) => {
console.error('Fetch error', error)
}
})
const handlePaginationChange = useCallback((page, pageSize) => {
setPage(page)
setPageSize(pageSize)
}, [])
return error
? <Error error={error} />
: (
<>
<Table
data={this.state.data}
loading={loading}
/>
<Pagination
page={page}
pageSize={pageSize}
onChange={handlePaginationChange}
/>
</>
)
}
複製代碼
固然,當 React 官方的 cache + AsyncComponent 組件 stable 後,咱們可能會有更完美的異步組件撰寫方式和編排體驗,關於 RxJS 與 React Hooks 結合,我也在個人 《使用 RxJS 與 React 實現一個 SQL 編輯器》進行了深一步的探究,感興趣的讀者能夠關注下,目前它在連載中