Stook-rest:React 異步數據管理思考

回顧

八個月前,我曾經寫過一篇文章 React 異步數據管理思考,當時我認爲使用 React Hooks 管理異步數據是一個更好的選擇。半年來我在項目中一直使用這種解決方案,發現這種方案的有點不少:TypeScript 支持度好、代碼量少且可讀性好、Loading 狀態獲取容易等。缺點是:1.異步數據的共享很差處理;2.組件承擔了太多的業務邏輯。前端

八個月後,通過多個項目的實踐,我建立了一個異步數據管理的工具:stook-restreact

關於 Stook-rest

異步數據管理一直是一個難點,在 React 的生態圈中,不少人把異步數據使用狀態管理維護,好比使用 Redux,用異步 Action 獲取遠程數據。我我的不喜歡使用 Redux 狀態管理維護異步數據,我更傾向於在組件內直接獲取異步數據,使用 hooks,簡化數據的獲取和管理。git

Stook-rest 是一個基於 Stook 的 Restful Api 數據獲取工具。github

基本用法

咱們使用 stook-restuseFetch 獲取數據,能夠輕鬆的拿到數據的狀態 { 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;

Edit bitter-frog-t2tbm

配置

全局配置

你可使用 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);
});

useFetch

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>
  )
}

Edit bitter-frog-t2tbm

URL(string)

HTTP 請求的 URL,eg: "/todos"。

options

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 對象,和原生 fetchbody 相似,不一樣的是,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 請求頭,和原生fetchHeaders 一致,但有默認值: { '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

結果 (Result)

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>
  );
};

咱們知道,paramsquerybody 三中參數值一般是一個對象,其實他們也能夠是一個函數,函數參數可讓咱們輕易地使用依賴請求。

依賴請求的方式能夠大大地減小你的代碼量,並讓你能夠用相似同步的代碼書寫數據請求代碼。

Edit sweet-lake-gu2el

數據共享

使用

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>
)

Edit wizardly-ellis-nqmqj

上面咱們在兩個組件中使用了 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>
)

Edit blue-glitter-zysrb

自定義 hooks

在真實的業務開發中,不建議直接在組件中使用 useFetch,更推薦是使用使用自定義 hooks 對請求的業務邏輯進行封裝。

如何自定義 hooks ?

const useFetchTodos = () => {
  const { loading, data: todos = [], error } = useFetch('/todos')
  return { loading, todos, error }
}

爲什麼推薦自定義 hooks ?

自定義 hooks 有下面幾點好處:

爲 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

爲了業務邏輯更好的複用,咱們常常會使用 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>
)

Refetch

不少場景中,你須要更新異步數據,好比在 CRUD 功能中,新增、刪除、修改、分頁、篩選等功能都須要更新異步數據。stook-rest 提供了三中方式更新數據,三種方式可在不一樣業務場景中使用,這是stook-rest的重要功能之一,你應該仔細閱讀並理解它的使用場景,使用這種方式管理異步數據,整個應用的狀態將變得更加簡單,代碼量會成本的減小,相應的可維護性大大增長。

從新獲取數據的三種方式

但不少時候,你須要更新異步數據,stook-rest提供三種方式更新數據:

  • 內部 Refetch
  • 更新依賴 deps
  • 使用 fetcher

內部 Refetch

這是最簡單的從新獲取數據的方式,一般,若是觸發更新的動做和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>
  )
}

Edit vigilant-bouman-y0gu7

更新依賴 deps

經過更新依賴來從新獲取數據,這也是經常使用的方式之一,由於在不少業務場景中,觸發動做會在其餘組件中,下面演示如何經過更新依賴觸發數據更新:

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>
  )
}

Edit loving-cray-b6xvq

你能夠在任意地方,無論組件內仍是組件外,你均可以更新依賴,從而實現數據更新。

注意:這裏的依賴是個對象,你必須更新整個對象的引用,若是你只更新對象的屬性是無效的。

使用 fetcher

有時候,你須要在組件外部從新獲取數據,但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>
)

Edit stoic-bardeen-y15mg

使用 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 管理異步數據,代碼很是簡潔,有一種大道至簡感受和返璞歸真感受。

相關文章
相關標籤/搜索