異步數據管理一直是前端的一個重點和難點,能夠這麼說,80%的 web 應用會有異步數請求據並在 UI 中消費,而且在至關多的 web 應用中,處理異步數據是它的核心業務邏輯。前端
在 React 的生態圈中,大部分人把異步數據使用狀態管理維護,好比使用 Redux,用異步 Action 獲取遠程數據,而後存在 store 中。react
但在這個時間節點,9012 年了,我認爲使用狀態管理去維護異步數據不是一種優雅的方式,React Hooks 出現後,我認爲直接在組件內維護異步數據更加合理。無論從開發效率仍是可維護性看,都比使用狀態管理好。git
爲何這說呢?下面咱們經過代碼來看看。github
如今,假設咱們要實現一個功能,獲取一個 TodoList 數據,而且用組件渲染。web
最簡單是直接在組件內使用生命週期獲取數據,而後存在組件內部的 state 中。json
import React from 'react'
class Todos extends React.Component {
constructor(props) {
super(props)
this.state = {
loading: false,
todos: [],
error: null,
}
}
async componentDidMount() {
this.setState({ loading: true })
try {
const todos = await (await fetch(
'https://jsonplaceholder.typicode.com/todos',
)).json()
this.setState({ todos, loading: false })
} catch (error) {
this.setState({ error, loading: false })
}
}
render() {
const { loading, todos, error } = this.state
if (loading) return <span>loading...</span>
if (error) return <span>error!</span>
return (
<ul> {todos.map(item => ( <li key={item.id}>{item.title}</li> ))} </ul>
)
}
}
複製代碼
這種方式很是很是符合人的直覺,但最大的問題是:外部沒法改變異步數據,組件渲染後數據就沒法再改變。這也是大部分人使用狀態管理維護異步數據的原因。異步
下面咱們看看如何使用 Redux 維護異步數據。async
假設咱們已經使用了 Redux 中間件 redux-thunk
,咱們會有下面相似的代碼:函數
首先,咱們會把字符串定義定義爲常量到一個 constant.js
export const LOADING_TODOS = 'LOADING_TODOS'
export const LOAD_TODOS_SUCCESS = 'LOAD_TODOS_SUCCESS'
export const LOAD_TODOS_ERROR = 'LOAD_TODOS_ERROR'
複製代碼
而後,編寫異步的 action, actions.js
:
import {
LOADING_TODOS,
LOAD_TODOS_SUCCESS,
LOAD_TODOS_ERROR,
} from '../constant'
export function fetchTodos() {
return dispatch => {
dispatch({ type: LOADING_TODOS })
return fetch('https://jsonplaceholder.typicode.com/todo')
.then(response => response.json())
.then(todos => {
dispatch({
type: LOAD_TODOS_SUCCESS,
todos,
})
})
.catch(error => {
dispatch({
type: LOAD_TODOS_ERROR,
error,
})
})
}
}
複製代碼
接着,在 reducer 中處理數據,todos.js
import {
LOADING_TODOS,
LOAD_TODOS_SUCCESS,
LOAD_TODOS_ERROR,
} from '../constant'
const initialState = {
loading: false,
data: [],
error: null,
}
export default function(state = initialState, action) {
switch (action.type) {
case LOADING_TODOS:
return {
...state,
loading: true,
}
case LOAD_TODOS_SUCCESS:
return {
...state,
data: action.todos,
loading: false,
}
case LOAD_TODOS_ERROR:
return {
...state,
error: action.error,
loading: false,
}
default:
return state
}
}
複製代碼
還沒完,最後,在組件中使用:
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { fetchTodos } from '../actions'
class Todos extends Component {
componentDidMount() {
const { dispatch } = this.props
dispatch(fetchTodos)
}
render() {
const { loading, items, error } = this.props
if (loading) return <span>loading...</span>
if (error) return <span>error!</span>
return (
<ul> {items.map(item => ( <li key={item.id}>{item.title}</li> ))} </ul>
)
}
}
const mapStateToProps = state => {
const { todos } = state
return {
loading: todos.loading,
items: todos.data,
error: todos.error,
}
}
export default connect(mapStateToProps)(Todos)
複製代碼
咱們能夠發現,使用 Redux 管理異步數據,代碼量激增,囉嗦冗餘,模板代碼一堆,,無論開發效率仍是開發體驗,亦或是能夠維護性和可讀性,我的認爲,相似的 redux 這樣的解決方案並不優雅。
下面咱們看看如何使用 React Hooks 獲取異步數據。
咱們使用 一個庫叫dahlia-rest
的 useFetch
獲取數據,能夠輕鬆的拿到數據的狀態 { loading, data, error }
,而後渲染處理:
import React from 'react'
import { useFetch } from 'dahlia-rest'
const Todos = () => {
const { loading, data, error } = useFetch(
'https://jsonplaceholder.typicode.com/todos',
)
if (loading) return <span>loading...</span>
if (error) return <span>error!</span>
return (
<ul> {data.map(item => ( <li key={item.id}>{item.title}</li> ))} </ul>
)
}
export defulat Todos
複製代碼
dahlia-rest
的完整用法能夠看 dahlia-rest
代碼很是簡潔,loading 狀態和錯誤處理很是優雅,也許你發現了,貌似這也和使用生命週期同樣,外部沒法改變數據狀態,其實不是的,下面重會點講講如何更新數據。
使用 hooks 維護異步數據,有三種方式更新異步數據,這裏用 dahlia-rest
舉例。
這是最簡單的從新獲取數據的方式,一般,若是觸發更新的動做和useFetch
在統一組件內,可使用這種方式。
const Todos = () => {
const { loading, data, error, refetch } = useFetch('/todos', {
query: { _start: 0, _limit: 5 }, // first page
})
if (loading) return <span>loading...</span>
if (error) return <span>error!</span>
const getSecondPage = () => {
refetch({
query: { _start: 5, _limit: 5 }, // second page
})
}
return (
<div> <button onClick={getSecondPage}>Second Page</button> <ul> {data.map(item => ( <li key={item.id}>{item.title}</li> ))} </ul> </div>
)
}
複製代碼
經過更新依賴來從新獲取數據,這也是經常使用的方式之一,由於在不少業務場景中,觸發動做會在其餘組件中,下面演示如何經過更新依賴觸發數據更新:
這裏使用一個簡單的狀態管理庫維護依賴對象,狀態管理的完整文檔請看dahlia-store。
定義一個 store 用來存放依賴:
// /stores/todoStore.ts
import { createStore } from 'dahlia-store'
const todoStore = createStore({
params: {
_start: 0,
_limit: 5,
},
updateParams(params) {
todoStore.params = params
},
})
複製代碼
在組件中,使用依賴:
import { observe } from 'dahlia-store'
import todoStore from '@stores/todoStore'
const Todos = observe(() => {
const { params } = todoStore
const { loading, data, error } = useFetch('/todos', {
query: params,
deps: [params],
})
if (loading) return <span>loading...</span>
if (error) return <span>error!</span>
const updatePage = () => {
todoStore.updateParams({ _start: 5, _limit: 5 })
}
return (
<div> <button onClick={updatePage}>Update Page</button> <ul> {data.map(item => ( <li key={item.id}>{item.title}</li> ))} </ul> </div>
)
})
複製代碼
你能夠在任意地方,無論組件內仍是組件外,你均可以能夠調用todoStore.updateParams
更新依賴,從而實現數據更新。
注意:這裏的依賴是個對象,你必須更新整個對象的引用,若是你只更新對象的屬性是無效的。
有時候,你須要在組件外部從新獲取數據,但useFetch
卻沒有任何能夠被依賴的參數,這時你可使用 fetcher
import { useFetch, fetcher } from 'dahlia/rest'
const Todos = () => {
const { loading, data, error } = useFetch('/todos', { name: 'GetTodos' })
if (loading) return <span>loading...</span>
if (error) return <span>error!</span>
return (
<ul> {data.map(item => ( <li key={item.id}>{item.title}</li> ))} </ul>
)
}
const Refresh = () => (
<button onClick={() => fetcher.GetTodos.refetch()}>refresh</button>
)
const TodoApp = () => (
<div> <Refresh /> <Todos /> </div>
)
複製代碼
使用 fetcher 是,你須要爲useFetch
提供 name 參數,用法是:fetcher['name'].refetch()
,這裏的 refetch
和內部 refetch
是同一個函數,因此它也有 options 參數。
我的認爲,異步數據不該該使用狀態管理來維護,應該放在組件內。對於大多數 web 應用,狀態管理中的數據應該是比較薄的一層,而且應該避免在狀態管理中處理異步帶來的反作用。也許,Redux 默認不支持處理異步數據,是一個至關有遠見的決定。
咱們發現,使用 Hooks 管理異步數據,代碼很是簡潔,有一種大道至簡感受和返璞歸真感受。幾行代碼就能寫完功能,爲何要搞出那麼長的鏈路,搞那麼繞的邏輯。