react-query管理服务端缓存

阎祖鹤
2023-12-01
一. 前因后果

React-Query是一个基于hooks的数据请求库。React-Query中的Query指一个异步请求的数据源。通过使用React-Query(或SWR)这样的数据请求库,可以将服务端状态从全局状态中解放出来。

按照来源,前端有两类 状态 需要管理:

  1. 用户交互的中间状态
  2. 服务端状态

在陈年的老项目中,通常用ReduxMobx这样的「全局状态管理方案」无差别对待他们。

事实上,他们有很大区别:

1. 用户交互的中间状态

比如组件的isLoadingisOpen,这类「状态」的特点是:

  1. 以「同步」的形式更新
  2. 状态」完全由前端控制
  3. 状态」比较独立(不同的组件拥有各自的isLoading)

这类「状态」通常保存在组件内部。

当「状态」需要跨组件层级传递,通常使用Context API

再大范围的「状态」会使用Redux这样的「全局状态管理方案」。

2. 服务端状态

当我们从服务端请求数据:

function App() { 
  const [data, updateData] = useState(null); 
   
  useEffect(async () => { 
    const data = await axios.get('/api/user'); 
    updateData(data); 
  }, []) 
 
  // 处理data 
} 

返回的数据通常作为「状态」保存在组件内部(如App组件的data状态)。

如果是需要复用的通用「状态」,通常将其保存在Redux这样的「全局状态管理方案」中。

这样做有2个坏处:

  1. 需要重复处理请求中间状态
    为了让App组件健壮,我们还需要处理请求中、出错等中间状态:
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); 
  }, []) 
 
  // 处理data 
} 

这类通用的中间状态处理逻辑可能在不同组件中重复写很多次。

  1. 缓存」的性质不同于「状态
    不同于交互的中间状态,服务端状态更应被归类为「缓存」,他有如下性质:
    • 通常以「异步」的形式请求、更新
    • 状态」由请求的数据源控制,不由前端控制
    • 状态」可以由不同组件共享

作为可以由不同组件共享的「缓存」,还需要考虑更多问题,比如:

  1. 缓存失效
  2. 缓存更新

Redux一把梭固然方便。但是,区别对待不同类型「状态」能让项目更可控。
这里,推荐使用React-Query管理服务端状态。

二、React-Query 介绍

React-Query是一个基于hooks的数据请求库。

我们可以将刚才的例子用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> 
  ) 
} 

React-Query中的Query指一个异步请求的数据源。

例子中userData字符串就是这个query独一无二的key

可以看到,React-Query封装了完整的请求中间状态(isLoadingisError…)。

不仅如此,React-Query还为我们做了如下工作:

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

数据的CRUD由2个hook处理:

  1. useQuery 处理数据的查
  2. useMutation 处理数据的增/删/改

在下面的例子中,点击「创建用户」按钮会发起创建用户的post请求:

import { useQuery, queryCache } from 'react-query'; 
 
unction App() { 
 const {data, isLoading, isError} = useQuery('userData', () => axios.get('/api/user')); 
 // 新增用户 
 const {mutate} = useMutation(data => axios.post('/api/user', data)); 
 
 return ( 
   <ul> 
     {data.map(user => <li key={user.id}>{user.name}</li>)} 
     <button 
       onClick={() => { 
         mutate({name: 'kasong', age: 99}) 
       }} 
     > 
       创建用户 
     </button> 
   </ul> 
 ) 

但是点击后userData query对应数据不会更新,因为他还未失效。

所以我们需要告诉React-QueryuserData query对应的缓存已经失效,需要更新:

import { useQuery, useQueryClient} from 'react-query'; 
 
function App() { 
  // ... 
  const queryClient = useQueryClient()
  const {mutate} = useMutation(userData => axios.post('/api/user', userData), { 
    onSuccess: () => { 
      queryClient .invalidateQueries('userData') 
    }   
  }) 
   
  // ... 
} 

通过调用mutate方法,会触发请求。

当请求成功后,会触发onSuccess回调,回调中调用queryClient .invalidateQueries,将userData对应的query缓存置为invalidate

这样,React-Query就会重新请求userData对应query的数据。

三、React-Query 辅助 Hook介绍
1、Hook:
	(1)useIsFetching
		返回应用程序在后台加载或获取的查询的数量(对于应用程序范围内的加载指示器很有用)。
		const isFetching = useIsFetching()	 返回所有数目
		const isFetching = useIsFetching(key,{配置过滤对象})	返回key开头的数目
		
		配置过滤对象:
			exact:true,	是否开启精确匹配
			active:ture,	匹配活跃查询
			inactive:true,	匹配不活跃查询
			stale:true,	 	匹配stale过时查询,否则匹配fresh新鲜查询
			fetching:true,	匹配正在拉取的查询,否则匹配未正在拉取的查询
			predicate: (query) =>布尔值,  匹配返回true的query
			queryKey:设置此属性以定义要匹配的查询键

	(2)useIsMutating
		返回应用程序正在获取的mutation数量
		和(1)使用方式相同

2、QueryClient:

	查询相关:
	
		(1)queryClient.fetchQuery
			异步获取,若缓存中没有或过时了,则会去获取最新结果,相当于同步的queryClient.setQueryData
			const data = await queryClient.fetchQuery(queryKey, queryFn,{配置对象})
			
			配置对象大部分和useQuery相同:除了enabled, refetchInterval, refetchIntervalInBackground, refetchOnWindowFocus, refetchOnReconnect, notifyOnChangeProps, notifyOnChangePropsExclusions, onSuccess, onError, onSettled, useErrorBoundary, select, suspense, keepPreviousData, placeholderData
			
		(2)queryClient.fetchInfiniteQuery
			无限获取,与fetchQuery类似,但可以用于获取和缓存无限查询。
			const data = await queryClient.fetchInfiniteQuery(queryKey, queryFn,{配置对象})
		
		(3)queryClient.prefetchQuery
			异步预获取,若缓存中没有或过时了,则会去获取最新结果
			const data = await queryClient.fetchQuery(queryKey, queryFn,{配置对象})
			配置对象和(1)相同
		
		(4)queryClient.prefetchInfiniteQuery
			无限预获取
			const data = await queryClient.prefetchInfiniteQuery(queryKey, queryFn,{配置对象})
		
		(5)queryClient.getQueryData
			从缓存中获取数据
			const data = queryClient.getQueryData(queryKey,{配置过滤对象})
		
		(6)queryClient.setQueryData
			设置缓存查询数据,如果在默认的5分钟的cacheTime中查询钩子没有利用该查询,则该查询将被垃圾收集
			queryClient.setQueryData(key,值/返回值的方法)
		
		(7)queryClient.getQueryState
			const state = queryClient.getQueryState(queryKey,{配置过滤对象})
		
		(8)queryClient.invalidateQueries
			使得匹配的查询失效并重新获取
			await queryClient.invalidateQueries(key,{
				exact:true, 精确匹配key
				refetchActive: true,	是否重新获取活跃查询			
				refetchInactive: true	是否重新获取不活跃查询
			},{
				throwOnError:true 	任何查询重取任务失败都会抛出错误
			})
		
		(9)queryClient.refetchQueries
			再次获取匹配的查询
			await queryClient.refetchQueries()
			await queryClient.refetchQueries(key,{配置过滤对象},{
			 	throwOnError:true	任何查询重取任务失败都会抛出错误
			})
		
		(10)queryClient.cancelQueries
			取消匹配的查询,若是取消axios/fetch等,需要添加额外的cancel属性,详情看之前的文章
			await queryClient.cancelQueries(key, {配置过滤对象})
		
		(11)queryClient.removeQueries
			从缓存中删除查询
			ueryClient.removeQueries(queryKey, {配置过滤对象})
		
		(12)queryClient.resetQueries
			将缓存的查询重置为初始状态,如果查询具有initialData,该查询的数据将被重置为该数据,如果查询是活动的会被重新获取
			ueryClient.resetQueries(queryKey, {配置过滤对象},{
				throwOnError:true	任何查询重取任务失败都会抛出错误
			})
		
		(13)queryClient.isFetching
			获取缓存中正在background-fetching, loading new pages, or loading more infinite query进行查询的条数
			const num=queryClient.isFetching();
			const num=queryClient.isFetching(key,{过滤配置对象});
		
		(14)queryClient.isMutating
			获取正在提交的mutation的条数
			const num=queryClient.isMutating();
			const num=queryClient.isMutating({过滤配置对象});
			
	配置相关:
	
		(15)queryClient.getDefaultOptions
			获取默认配置,从QueryClient中设置的和setDefaultOptions方法设置的内容
			const defaultOptions = queryClient.getDefaultOptions()
			
		(16)queryClient.setDefaultOptions
			动态设置QueryClient的默认选项。
			 queryClient.setDefaultOptions({
			   queries: {		
			     staleTime: Infinity,
			   },
			 })
		
		(17)queryClient.getQueryDefaults
			获取指定查询的配置
			const defaultOptions = queryClient.getQueryDefaults(key)
		
		(18)queryClient.setQueryDefaults
			设置指定查询配置
			queryClient.setQueryDefaults(key, { queryFn: fetchPosts })
			const { data } = useQuery(key)
		
		(19)queryClient.getMutationDefaults
			获取指定mutation的配置
			const defaultOptions = queryClient.getMutationDefaults(key)
		
		(20)queryClient.setMutationDefaults
			设置指定mutation的配置
			queryClient.setMutationDefaults(key, { mutationFn: addPost })
			const { data,mutate } = useMutation('addPost')
		
	缓存设置:
	
		(21)queryClient.getQueryCache
			获取缓存了的查询
			const queryCache = queryClient.getQueryCache()
		
		(22)queryClient.getMutationCache
			获取缓存了的mutation
			const mutationCache = queryClient.getMutationCache()
			
		(23)queryClient.clear
			清除缓存
			queryClient.clear()


3、QueryClientProvider:
	使用QueryClientProvider组件连接并提供一个QueryClient到你的应用程序:
	
	<QueryClientProvider client={queryClient} contextSharing={false}>
		contextSharing:默认为false,若为true,将共享第一个和至少一个实例的上下文窗口,以确保在不同的包或microfrontends他们都使用相同的实例的上下文,不管模块范围。
		...
	</QueryClientProvider>
	

4、useQueryClient:
	返回当前QueryClient的实例
	
	const queryClient = useQueryClient()

5、QueryCache
	存储包含的所有数据、元信息和查询状态,通常是使用queryClient对缓存的操作,而不是直接操作
	const queryCache = new QueryCache({
	   onError: (error,query) => {	某个查询遇到错误触发
	     console.log(error)
	   }
	})
	
	方法:
	    .find(key)					用于从缓存获取现有的查询实例,包含查询的所有状态,还包含所有实例和查询的底层内容
	    .findAll(key,{过滤配置对象})	返回数组集合
	    .subscribe((query)=>{...})	监听订阅缓存中的所有查询,如查询状态更改或查询被更新、添加或删除
	    	const unsubscribe = queryCache.subscribe(callback);	返回一个取消订阅方法
	    .clear()	完全清除缓存并重新开始

6、MutationCache
	存储mutations,通常是使用queryClient对缓存的操作,而不是直接操作
	
	 const mutationCache = new MutationCache({
	   onError: (error,variables, context, mutation) => {	提交错误时触发 
	     console.log(error)
	   }
	 })
	
	方法:
		.getAll()	返回缓存中所有的mutations
	    .subscribe.((mutation)=>{	监听订阅缓存中所有的mutation,如变化的mutation状态或正在更新、添加或删除的mutation
	    	返回一个取消监听订阅的方法
	    	...
	    })
	    	const unsubscribe = mutationCache.subscribe(callback)
	    .clear()	完全清除缓存并重新启动。

四、参考博文
  1. https://developer.51cto.com/art/202102/646085.htm
  2. https://blog.csdn.net/weixin_43294560/article/details/115049649
  3. https://react-query.tanstack.com/overview
 类似资料: