八個月前,我曾經寫過一篇文章 React 異步數據管理思考,當時我認爲使用 React Hooks 管理異步數據是一個更好的選擇。半年來我在項目中一直使用這種解決方案,發現這種方案的有點不少:TypeScript 支持度好、代碼量少且可讀性好、Loading 狀態獲取容易等。缺點是:1.異步數據的共享很差處理;2.組件承擔了太多的業務邏輯。前端
八個月後,通過多個項目的實踐,我建立了一個異步數據管理的工具:stook-rest。react
異步數據管理一直是一個難點,在 React 的生態圈中,不少人把異步數據使用狀態管理維護,好比使用 Redux,用異步 Action 獲取遠程數據。我我的不喜歡使用 Redux 狀態管理維護異步數據,我更傾向於在組件內直接獲取異步數據,使用 hooks,簡化數據的獲取和管理。git
Stook-rest 是一個基於 Stook 的 Restful Api 數據獲取工具。github
咱們使用 stook-rest
的 useFetch
獲取數據,能夠輕鬆的拿到數據的狀態 { loading, data, error }
,而後渲染處理:json
import React from "react"; import { useFetch } from "stook-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 default Todos;
你可使用 config
方法進行全局配置,全局配置將在每一個請求生效:後端
import { config } from "stook-rest"; config({ baseURL: "https://jsonplaceholder.typicode.com", headers: { foo: "bar" } });
baseURL
: stringapi
Restful Api 服務器 baseURL, 默認爲當前前端頁面 host。數組
headers
: object服務器
每一個請求都會帶上的請求頭,默認爲 { 'content-type': 'application/json; charset=utf-8' }
app
在某些應用場景,你能夠能有多個後端服務,這時你須要多個 Client 實例:
const client = new Client({ baseURL: "https://jsonplaceholder.typicode.com", headers: { foo: "bar" } }); client.fetch("/todos").then(data => { console.log(data); });
const result = useFetch(url, options)
以簡單高效的方式獲取和管理異步數據是 stook-rest 的核心功能。下面是一個示例:
import { useFetch } from 'stook-rest' interface Todo { id: number title: string completed: boolean } const Todos = () => { const { loading, data, error } = useFetch<Todo[]>('/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> ) }
HTTP 請求的 URL,eg: "/todos"。
method?: Method
HTTP 請求的類型,默認爲 GET
, 所有可選值: type Method = 'GET' | 'POST' | 'DELETE' | 'PUT' | 'PATCH' | 'HEAD'
const { loading, data, error } = useFetch<Todo[]>('/todos', { method: 'POST' })
query?: Query
HTTP 請求的 query 對象,一般 GET
類型的請求比較經常使用。
const { loading, data, error } = useFetch<Todo[]>('/todos', { query: { pageNum: 1, pageSize: 20 } })
上面會把 url 轉換爲: /todos?pageNum=1&pageSize=20
。詳細的轉換規則請參照 qs
body?: Body
HTTP 請求的 body 對象,和原生 fetch
的 body 相似,不一樣的是,useFetch
的 body 支持 JS 對象:
const { loading, data, error } = useFetch("/todos", { body: { title: "todo1" } });
params?: Params
URL 的參數對象,用法以下:
const { loading, data, error } = useFetch("/todos/:id", { params: { id: 10 } });
請求發送後, /todos/:id
會轉換爲 /todos/10
。
headers?: HeadersInit;
HTTP 請求頭,和原生fetch
的 Headers
一致,但有默認值: { 'content-type': 'application/json; charset=utf-8' }
deps?: Deps
useFetch
是一個自定義的 React hooks,默認狀況下,組件屢次渲染,useFetch
只會執行一次,不過若是你設置了依賴 (deps),而且依賴發生更新,useFetch
會從新執行,就是會從新獲取數據,其機制相似於 useEffect
的依賴,不一樣的是不設置任何依賴值時,當組件發生屢次渲染,useFetch
只會執行一次,useFetch
執行屢次。
依賴值 deps 是個數組,類型爲:type Deps = ReadonlyArray<any>
key?: string
該請求的惟一標識符,由於 stook-rest 是基於 stook,這個 key 就是 stook 的惟一 key,對於 refetch 很是有用。默認是爲 ${method} ${url}
,好比請求以下:
const { loading, data } = useFetch("/todos", { method: "POST" });
那默認的 key 爲: POST /todos
loading: boolean
一個布爾值,表示數據是否加載中。
data: T
服務器返回的數據。
error: RestError
服務器返回錯誤。
refetch: <T>(options?: Options) => Promise<T>
從新發起一個請求獲取數據,eg:
const Todos = () => { const { loading, data, error, refetch } = useFetch<Todo[]>("/todos"); if (loading) return <span>loading...</span>; if (error) return <span>error!</span>; return ( <div> <button onClick={refetch}>Refetch</button> <ul> {data.map(item => ( <li key={item.id}>{item.title}</li> ))} </ul> </div> ); };
不少時候,一個請求會依賴另一個請求的數據,這時候請求會有先後順序,stook-rest 能夠很是優雅的處理這種依賴請求:
import React from "react"; import { config, useFetch } from "stook-rest"; export default () => { const { data: todos } = useFetch("/todos"); const { loading, data: todo } = useFetch("/todos/:id", { params: () => ({ id: todos[9].id }) }); if (loading) return <div>loading....</div>; return ( <div className="App"> <div>Todo:</div> <pre>{JSON.stringify(todo, null, 2)}</pre> </div> ); };
咱們知道,params
、query
、body
三中參數值一般是一個對象,其實他們也能夠是一個函數,函數參數可讓咱們輕易地使用依賴請求。
依賴請求的方式能夠大大地減小你的代碼量,並讓你能夠用相似同步的代碼書寫數據請求代碼。
stook-rest 另外一個強大的特性是請求數據的共享,因爲 stook-rest 底層的數據管理是基於 stook 的,因此跨組件共享數據將變得很是簡單:
const TodoItem = () => { const { loading, data: todo } = useFetch('/todos/1') if (loading) return <div>loading....</div> return ( <div> <pre>{JSON.stringify(todo, null, 2)}</pre> </div> ) } const ReuseTodoItem = () => { const { loading, data: todo } = useFetch('/todos/1') if (loading) return <div>loading....</div> return ( <div> <div>ReuseTodoItem:</div> <pre>{JSON.stringify(todo, null, 2)}</pre> </div> ) } export default () => ( <div> <TodoItem></TodoItem> <ReuseTodoItem></ReuseTodoItem> </div> )
上面咱們在兩個組件中使用了 useFetch
,它們的惟一 key 是同樣的 (都是 GET /todos/1
),並且只會發送一次請求,兩個組件會使用同一份數據。
我的不太建議直接在多個組件使用同一個 useFetch
,更進一步使用自定義 hooks,加強業務邏輯的複用性:
const useFetchTodo = () => { const { loading, data: todo, error } = useFetch('/todos/1') return { loading, todo, error } } const TodoItem = () => { const { loading, todo } = useFetchTodo() if (loading) return <div>loading....</div> return ( <div> <div>TodoItem:</div> <pre>{JSON.stringify(todo, null, 2)}</pre> </div> ) } const ReuseTodoItem = () => { const { loading, todo } = useFetchTodo() if (loading) return <div>loading....</div> return ( <div> <div>ReuseTodoItem:</div> <pre>{JSON.stringify(todo, null, 2)}</pre> </div> ) } export default () => ( <div> <TodoItem></TodoItem> <ReuseTodoItem></ReuseTodoItem> </div> )
在真實的業務開發中,不建議直接在組件中使用 useFetch
,更推薦是使用使用自定義 hooks 對請求的業務邏輯進行封裝。
const useFetchTodos = () => { const { loading, data: todos = [], error } = useFetch('/todos') return { loading, todos, error } }
自定義 hooks 有下面幾點好處:
這看上去和直接使用 useFetch
沒有太大區別,實際上它增長了代碼的可讀性。
若是咱們咱們直接在組件中使用 useFetch
,咱們須要在組件引入很是多文件。這個請求數據只有一個組件使用還好,若是多個組件須要共享此請求數據,文件管理將會很是亂。
import React from 'react' import { useFetch } from 'stook-rest' import { Todo } from '../../typings' import { GET_TODO } from '../../URL.constant' export default () => { const { data: todos } = useFetch<Todo[]>(GET_TODO) if (loading) return <div>loading....</div> return ( <div className="App"> <div>Todo:</div> <pre>{JSON.stringify(todo, null, 2)}</pre> </div> ) }
若是使用使用自定義 hooks,咱們只需在組件中引入 hooks:
import React from 'react' import { useFetchTodos } from '../../useFetchTodos' export default () => { const { loading, todos } = useFetchTodos() if (loading) return <div>loading....</div> return ( <div className="App"> <div>Todos:</div> <pre>{JSON.stringify(todos, null, 2)}</pre> </div> ) }
爲了業務邏輯更好的複用,咱們常常會使用 computed value:
const useFetchTodos = () => { const { loading, data: todos = [], error } = useFetch<Todo[]>('/todos') const count = todos.length const completedCount = todos.filter(i => i.completed).length return { loading, todos, count, completedCount, error } }
自定義 hooks 讓數據跨組件共享數據更加優雅:
interface Todo { id: number title: string completed: boolean } const useFetchTodos = () => { const { loading, data: todos = [], error } = useFetch<Todo[]>('/todos') const count = todos.length const completedCount = todos.filter(i => i.completed).length return { loading, todos, count, completedCount, error } } const TodoList = () => { const { loading, todos, count, completedCount } = useFetchTodos() if (loading) return <div>loading....</div> return ( <div> <div>TodoList:</div> <div>todos count: {count}</div> <div>completed count: {completedCount}</div> <pre>{JSON.stringify(todos, null, 2)}</pre> </div> ) } const ReuseTodoList = () => { const { loading, todos, count, completedCount } = useFetchTodos() if (loading) return <div>loading....</div> return ( <div> <div>ReuseTodoList:</div> <div>todos count: {count}</div> <div>completed count: {completedCount}</div> <pre>{JSON.stringify(todos, null, 2)}</pre> </div> ) } export default () => ( <div style={{ display: 'flex' }}> <TodoList></TodoList> <ReuseTodoList></ReuseTodoList> </div> )
不少場景中,你須要更新異步數據,好比在 CRUD 功能中,新增、刪除、修改、分頁、篩選等功能都須要更新異步數據。stook-rest
提供了三中方式更新數據,三種方式可在不一樣業務場景中使用,這是stook-rest
的重要功能之一,你應該仔細閱讀並理解它的使用場景,使用這種方式管理異步數據,整個應用的狀態將變得更加簡單,代碼量會成本的減小,相應的可維護性大大增長。
但不少時候,你須要更新異步數據,stook-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> ) }
經過更新依賴來從新獲取數據,這也是經常使用的方式之一,由於在不少業務場景中,觸發動做會在其餘組件中,下面演示如何經過更新依賴觸發數據更新:
import { useState } from 'react' import { useFetch } from 'stook-rest' export default () => { const [count, setCount] = useState(1) const { loading, data, error } = useFetch('/todos', { deps: [count], }) if (loading) return <span>loading...</span> if (error) return <span>error!</span> const update = () => { setCount(count + 1) } return ( <div> <button onClick={update}>Update Page</button> <ul> {data.map(item => ( <li key={item.id}>{item.title}</li> ))} </ul> </div> ) }
你能夠在任意地方,無論組件內仍是組件外,你均可以更新依賴,從而實現數據更新。
注意:這裏的依賴是個對象,你必須更新整個對象的引用,若是你只更新對象的屬性是無效的。
有時候,你須要在組件外部從新獲取數據,但useFetch
卻沒有任何能夠被依賴的參數,這時你可使用 fetcher:
import { useFetch, fetcher } from 'stook-rest' const Todos = () => { const { loading, data, error } = useFetch('/todos', { key: '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.get('GetTodos').refetch()}>refresh</button> const TodoApp = () => ( <div> <Refresh /> <Todos /> </div> )
使用 fetcher 是,你須要爲useFetch
提供 name 參數,用法是:fetcher['name'].refetch()
,這裏的 refetch
和內部 refetch
是同一個函數,因此它也有 options 參數。
使用 fetcher 時,爲一個 HTTP 請求命名 (name) 不是必須的,每一個 HTTP 請求都有一個默認的名字,默認名字爲該請求的 url 參數。
爲了項目代碼的可維護性,推薦把因此 Api 的 url 集中化,好比:
// apiService.ts enum Api { GetTodo = 'GET /todos/:id', GetTodos = 'GET /todos', } export default Api
在組件中:
import { useFetch, fetcher } from 'stook-rest' import Api from '@service/apiService' const Todos = () => { const { loading, data, error } = useFetch(Api.GetTodos) if (loading) return <span>loading...</span> if (error) return <span>error!</span> return ( <div> <button onClick={() => fetcher[Api.GetTodos]refetch()}>refresh</button> <ul> {data.map(item => ( <li key={item.id}>{item.title}</li> ))} </ul> </div> ) }
我的認爲,使用 Hooks 獲取和管理異步數據,將逐漸在 React 社區中流行。咱們發現,使用 Hooks 管理異步數據,代碼很是簡潔,有一種大道至簡感受和返璞歸真感受。