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' }markdown

建立实例

在某些应用场景,你能够能有多个后端服务,这时你须要多个 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 管理异步数据,代码很是简洁,有一种大道至简感受和返璞归真感受。

相关文章
相关标签/搜索