異步數據管理一直是前端的一個重點和難點,能夠這麼說,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 管理異步數據,代碼很是簡潔,有一種大道至簡感受和返璞歸真感受。幾行代碼就能寫完功能,爲何要搞出那麼長的鏈路,搞那麼繞的邏輯。