React-Query 让你的状态管理更优雅

下面就是一个简单的从服务端获取数据的案例

function App() {

  const [data, updateData] = useState(null);
  const [isError, setError] = useState(false);
  const [isLoading, setLoading] = useState(false);
  
  useEffect(async () => {
    setError(false);
    setLoading(true);
    try {
      const data = await axios.get('/api/user');
      updateData(data);
    } catch(e) {
      setError(true);
    }
    setLoading(false);
  }, [])

}

在使用 React Hooks 编写组件时,我们常需要手动维护来自服务器的处理状态。在日常开发中引起一些麻烦。 处理异步数据时,我们需要考虑很多事情,例如更新,缓存或重新获取。 使用 React-Query 能够更高效的帮你管理服务端的状态。

React-Query 是什么

这是一个适用于 React Hooks 的请求库。 这个库将帮助你获取、同步、更新和缓存你的远程数据, 提供两个简单的 hooks,就能完成增删改查等操作React-Query 使用声明式管理服务端状态,可以使用零配置开箱即用地处理缓存,后台更新和陈旧数据。无需繁琐的配置,编写useReduce,以及维护全局状态,只要知道如何使用 Promise 或 async / await ,传递一个可解析数据(或引发错误)的函数,剩下的交给 React-Query 就好了,使用更少的代码获得更高的效率.

开始使用 React-Query

安装 React-Query

 $ npm i react-query
 # or
 $ yarn add react-query

下面我们用 React-Query改写


 import { useQuery } from 'react-query'
 
 function App() {

   const {data, isLoading, isError} = useQuery('userData', () => axios.get('/api/user'));
   
   if (isLoading) {
     return <div>loading</div>;
   }
   
   return (
     <ul>
       {data.map(user => <li key={user.id}>{user.name}</li>)}
     </ul>
   )
 }

例子中 "userData" 字符串就是这个 query 独一无二的key。 可以看到,React-Query封装了完整的请求中间状态(isLoading、isError...)。 不仅如此,React-Query还为我们做了如下工作:

  1. 多个组件请求同一个query时只发出一个请求
  2. 缓存数据失效/更新策略(判断缓存合适失效,失效后自动请求数据)
  3. 对失效数据垃圾清理

React-Query 中的三个重要知识点

常用参数配置

  • staleTime 重新获取数据的时间间隔 默认0
  • cacheTime 数据缓存时间 默认 1000 60 5 5分钟
  • retry 失败重试次数 默认 3次
  • refetchOnWindowFocus 窗口重新获得焦点时重新获取数据 默认 false
  • refetchOnReconnect 网络重新链接
  • refetchOnMount 实例重新挂载
  • enabled 如果为“false”的化,“useQuery”不会触发,需要使用其返回的“refetch”来触发操作 如何全局配置呢?如下:
import { ReactQueryConfigProvider, ReactQueryProviderConfig } from 'react-query';

const queryConfig: ReactQueryProviderConfig = {
  /**
   * refetchOnWindowFocus 窗口获得焦点时重新获取数据
   * staleTime 过多久重新获取服务端数据
   * cacheTime 数据缓存时间 默认是 5 * 60 * 1000 5分钟
   */
  queries: { 
    refetchOnWindowFocus: true,
    staleTime: 5 * 60 * 1000, 
    retry: 0
  },
};

ReactDOM.render(
    <ReactQueryConfigProvider config={queryConfig}>
        <App />
    </ReactQueryConfigProvider>
    document.getElementById('root')
  );
也可以单独配置,如下:

function Todos() {
   // 第三个参数即可传参了
   // "enabled"参数为false的化,不会自动发起请求,而是需要调用“refetch”来触发
   const {
     isIdle,
     isLoading,
     isError,
     data,
     error,
     refetch,
     isFetching,
   } = useQuery('todos', fetchTodoList, {
     enabled: false,
   })
 
   return (
     <>
       <button onClick={() => refetch()}>Fetch Todos</button>
 
       {isIdle ? (
         'Not ready...'
       ) : isLoading ? (
         <span>Loading...</span>
       ) : isError ? (
         <span>Error: {error.message}</span>
       ) : (
         <>
           <ul>
             {data.map(todo => (
               <li key={todo.id}>{todo.title}</li>
             ))}
           </ul>
           <div>{isFetching ? 'Fetching...' : null}</div>
         </>
       )}
     </>
   )
 }

数据查询与操作

useQuery(查)查询数据 (Get)

基本使用方法

function Todos() {
   // useQuery的第一个参数,作为useQuery查询的唯一标识,该值唯一
   // 可以是string、array、object
   // string -> useQuery('todos', ...) queryKey === ['todos']
   // array -> useQuery(['todo', 5], ...) queryKey === ['todo', 5]
   // object -> useQuery(['todo', 5, { preview: true }], ...)  queryKey === ['todo', 5, { preview: true }]
   const { isLoading, isError, data, error } = useQuery('todos', fetchTodoList)

   if (isLoading) {
     return <span>Loading...</span>
   }

   if (isError) {
     return <span>Error: {error.message}</span>
   }

   // also status === 'success', but "else" logic works, too
   return (
     <ul>
       {data.map(todo => (
         <li key={todo.id}>{todo.title}</li>
       ))}
     </ul>
   )
 }

传递参数

function Todos({ completed }) {
    // useQuery(['todo', { status: 1, page: 1 }], ...)  queryKey === ['todo', { status: 1, page: 1 }]
    // 传递参数给“fetchTodoList”使用
   const queryInfo = useQuery(['todos', { status: 1, page: 1 }], fetchTodoList)
 }

 // 函数参数
 // key -> “todos”
 // status -> 1 page -> 1
 function fetchTodoList(key, { status, page }) {
   return new Promise()
   // ...
 }

该库还实现了常用的查询操作:

useMutation(增、改、删)操作数据 (Post,Delete,Patch,Put

// 当“mutate()”被调用时,执行“pingMutation”
const PingPong = () => {
   const [mutate, { status, data, error }] = useMutation(pingMutation)

   const onPing = async () => {
     try {
       const data = await mutate()
       console.log(data)
     } catch {
     }
   }
   return <button onClick={onPing}>Ping</button>
 }

传递参数

// "mutate({title})"就会将参数“title”传递给“createTodo”函数了
const createTodo = ({ title }) => {
  console.log("title ", title)
}

const CreateTodo = () => {
const [title, setTitle] = useState('')
const [mutate] = useMutation(createTodo)

const onCreateTodo = async e => {
    e.preventDefault()

    try {
    await mutate({ title })
    // Todo was successfully created
    } catch (error) {
    // Uh oh, something went wrong
    }
}

return (
    <form onSubmit={onCreateTodo}>
    <input
        type="text"
        value={title}
        onChange={e => setTitle(e.target.value)}
    />
    <br />
    <button type="submit">Create Todo</button>
    </form>
)
}

清除缓存

每当我们编辑完一篇文章,返回列表页面,如果不清除缓存,那么数据还是缓存的数据,所以需要清除缓存,使得“userQuery”失效,回到列表页的时候重新拉取最新数据

参考代码如下

import { useMutation, useQueryCache } from 'react-query'

const queryCache = useQueryCache()

const [mutate] = useMutation(addTodo, {
    onSuccess: () => {
        // invalidateQueries 的匹配规则
        // eg:
        // queryCache.invalidateQueries('todos') 那么如下两个`query key`都会被匹配到,匹配到的缓存都会失效
        // const todoListQuery = useQuery('todos', fetchTodoList)
        // const todoListQuery = useQuery(['todos', { page: 1 }], fetchTodoList)
        queryCache.invalidateQueries('todos')
        queryCache.invalidateQueries('reminders')
    },
})

Devtools 配套开发工具

导入开发工具

import { ReactQueryDevtools } from 'react-query/devtools'

默认情况下,当process.env.NODE ENV === 'production' 时开启 Devtools ,不必担心构建时需要排除他们

浮动模式下开启,会将devtools作为固定的浮动元素安装在开发的应用程序中,并在屏幕一角提供一个切换按钮以显示和隐藏devtools。

尽可能将以下代码放在您的React应用程序中。 它离页面根目录越近,效果越好!

 import { ReactQueryDevtools } from 'react-query/devtools'
 
 function App() {
   return (
     <QueryClientProvider client={queryClient}>
       {/* The rest of your application */}
       <ReactQueryDevtools initialIsOpen={false} />
     </QueryClientProvider>
   )
 }

React-Query 周边生态

React-Query 周边生态满足日常开发需求,支持 TypeScript ,GraphQL 。并可在 React Native 中使用,同时也兼容服务端渲染方案 SSR 支持 Next.js 的使用

其他方案对比

  1. react-query 对于 mutation 可使用 hooks,支持更多选项 (如 keepPreviousData),功能更多,更适合 API 复杂的项目
  2. swr 相对轻量,可随处使用,不需要在父组件设置 Provider 。更加轻便

官网相关方案对比表格

React QuerySWR (Website)Apollo Client (Website)RTK-Query (Website)
Github Repo / Stars18K16K16K372
Platform RequirementsReactReactReact, GraphQLRedux
Their Comparison(none)(none)Comparison
Supported Query SyntaxPromise, REST, GraphQLPromise, REST, GraphQLGraphQLPromise, REST, GraphQL
Supported FrameworksReactReactReact + OthersAny
Supported Query KeysJSONJSONGraphQL QueryJSON
Query Key Change DetectionDeep Compare (Stable Serialization)Referential Equality (===)Deep Compare (Unstable Serialization)Referential Equality (===)
Query Data Memoization LevelQuery + Structural SharingQueryQuery + Entity + Structural SharingQuery
Bundle Size11.2KB5.0KB33.9KB10.3KB
API DefinitionOn-Use, DeclarativeOn-UseGraphQL SchemaDeclarative
Queries
Caching
Devtools🟡
Polling/Intervals
Parallel Queries
Dependent Queries
Paginated Queries
Infinite Queries🛑
Bi-directional Infinite Queries🔶🔶🛑
Infinite Query Refetching🛑🛑
Lagged Query Data1🛑🛑
Selectors🛑
Initial Data
Scroll Recovery
Cache Manipulation
Outdated Query Dismissal
Render Optimization2🛑🛑
Auto Garbage Collection🛑🛑
Mutation Hooks🟡
Offline Mutation Support🛑🟡🛑
Prefetching APIs🔶
Query Cancellation🛑🛑🛑
Partial Query Matching3🛑🛑
Stale While Revalidate🛑
Stale Time Configuration🛑🛑🛑
Pre-usage Query/Mutation Configuration4🛑🛑
Window Focus Refetching🛑🛑
Network Status Refetching🛑
General Cache Dehydration/Rehydration🛑
Offline Caching✅ (Experimental)🛑🔶
React Suspense (Experimental)🛑🛑
Abstracted/Agnostic Core🛑
Automatic Refetch after Mutation5🔶🔶
Normalized Caching6🛑🛑🛑

更多查看

总结

使用 React-Query 可以更加高效的管理来自服务端的请求状态,用更少的代码实现较为复杂的需求,让你的状态管理更优雅。

参考链接:

React-Query 官网

用 react-query 解决你一半的状态管理问题

react-query