使用React-Query管理状态


在React中处理请求时我们经常会写出这样的代码

export const Component = () => {
    const [isLoading, setIsLoading] = useState(true)
    const [data, setData] = useState([])
    const [error, setError] = useState('')

    return(
        <>
        </>
    )
}

随着代码规模增加,内部状态会变得极为混乱,维护成本会变得很高,而React-Query就是为了解决这种问题。

什么是React-Query?React Query is often described as the missing data-fetching library for React, but in more technical terms, it makes fetching, caching, synchronizing and updating server state in your React applications a breeze

如果使用React-Query,上面的代码就可以改成下面这样

export const Component = () => {
    const { isLoading, data, isError, error, isFetching} = useQuery(key, func,config)

    return(
        <>
        </>
    )
}

显然,对于异步请求的状态管理会变得十分容易,除此之外,React-Query还有许多优秀的特性。

基础api

useQuery

首先使用React-Query进行一次简单的请求

function fetchSuperHeroes() {
    return axios.get('http://localhost:3001/superheroes')
}

export const QueryTest = () => {

    const { isLoading, data, isError, error, isFetching} = useQuery('super-heroes', fetchSuperHeroes,{
        staleTime: 50000
    })
    return(
        <>
        </>
    )
}

React-Query内部会建立一个缓存,缓存的key由使用者指定,这里userQuery的第一个参数就是key,返回的data就是value。useQuery是最基础的请求hook,它接受的三个参数分别是key,返回promise的请求函数,config。

useMutation

useMutation常用来发送/跟新服务端数据

//使用useMutation
const { isIdle, isLoading, isError, isSuccess, error, data} = useMutation(newTodo => {
    return axios.post('/todos', newTodo)
})

特性

staleTime && cacheTime

前面提到了React-Query会依靠context建立缓存,对于每一个key-value,RQ会设置一个staleTime,在staleTime内认为该数据是fresh(defaultConfig下staleTime为0),下一次useQuery直接使用缓存数据而不会发起请求。

而cacheTime是指该缓存的有效期,超过cacheTime后数据会被清理,默认情况下cacheTime是5min

staleTime和cacheTime在使用时到底有什么区别呢?
想弄清区别必须先了解isLoading和isFetching,看一下demo

//父组件
function Father() {
    const { isLoading,data, isError, error, isFetching} = useQuery('super-heroes', fetchSuperHeroes)

    if(isLoading){
        return<h2>Loaidng...</h2>
    }

    if(isFetching){
        return<h2>Fetching...</h2>
    }

    return (
        <div>
            <Router>
            <Link to='child'>Child</Link>
              <Routes>
                <Route path='/child' element={<Child />} />
              </Routes>
            </Router>
        </div>
  );
}

//子组件
export function Child() {
    const { isLoading, data, isError, error, isFetching } = useQuery('super-heroes', fetchSuperHeroes)

    if (isLoading) {
        return <h2>Loaidng...</h2>
    }

    if (isFetching) {
        return <h2>Fetching...</h2>
    }
}

userQuery使用defaultConfig,服务端统一设置延迟2s

在首次进入Father时由于cache内没有数据,userQuery发起请求,此时isLoading为true,主页显示Loading。进入Child时,由于默认配置下data已经过期,此时ReactQuery将过期的数据先返还给使用者,然后后台发起fetch进入isFetching状态,最后用新数据更新组件状态。

如果尝试更改staleTime,进入Child时如果数据为fresh,useQuery并不会发起请求,而是直接缓存数据。

tip:在cache有效的情况下同一组件内触发rerender并不会重新请求,而是直接返回cache数据

总结一下staleTIme和cacheTime的关系

time

默认配置

  • 默认情况下userQuery返回的数据staleTime为0,即每次返回的数据都视为过期,可以通过config设置staleTime
  • 默认cacheTime是5min,5min过后数据会被清理
  • 失败的query会重复3次
  • 窗口重新获得focus时会重新发起请求

更多defaultConfig可以参考https://react-query.tanstack.com/guides/important-defaults

示例

窗口获得Focus时进行查询

可分别在全局config或者useQuery内进行配置

//global
const queryClient = new QueryClient({
    defaultOptions: {
        queries: {
            refetchOnWindowFocus: false,
        },
    },
})
 
function App() {
    return <QueryClientProvider client={queryClient}></QueryClientProvider>
}

//single
useQuery(url, func, { refetchOnWindowFocus: false })

手动refetch

很多情况下并不需要组件mount后查询数据,而是通过交互进行查询,比如click事件

function App() {
    const { isLoading,data, isError, error, isFetching, refetch} = useQuery('super-heroes', fetchSuperHeroes,     {enabled: false})

    if(isLoading){
      return<h2>Loaidng...</h2>
    }

    if(isFetching){
      return<h2>Fetching...</h2>
    }

    return (
        <div>
          <button onClick={() => {refetch()}}>Refetch</button>
        </div>
    );
}

首先设置enabled为false,这样mount时不会发起查询,在点击时调用refetch即可,第一次点击时显示loading,第二次显示fetching

定时查询

使用refetchInterval,refetchIntervalInBackground可以实现间隔一定时间查询一次, 这里指定间隔2.5s查询一次

function App() {
    const { isLoading,data, isError, error, isFetching, refetch} = useQuery('super-heroes', fetchSuperHeroes, { refetchInterval: 2500,refetchIntervalInBackground: true})

    if(isLoading){
        return<h2>Loaidng...</h2>
    }

    if(isFetching){
        return<h2>Fetching...</h2>
    }

    return (
        <div>
            Hello
        </div>
    );
}

在fetch success或error时进行回调

config内提供了onSuccess和onError设置回调函数

const { isLoading, data, isError, error, isFetching} = useQuery('super-heroes', fetchSuperHeroes,{
    onSuccess:() => {console.log('success')},
    onError: () => {console.log('error')}
})

手动设置数据

除了使用useQuery,还可以使用函数直接设置cache内数据

queryClient.getQueryState(queryKey)
queryClient.setQueryState(queryKey, data)

使用invalidateQueries强制使数据过期

queryClient.invalidateQueries(queryKey)

Optimistic Updates

官方文档的例子写的很好

useMutation(updateTodo, {
   onMutate: async newTodo => {
     // 取消已经发出的查询,防止修改本地cache
     await queryClient.cancelQueries(['todos', newTodo.id])
 
     // 缓存Optimistic Updates前的数据
     const previousTodo = queryClient.getQueryData(['todos', newTodo.id])
 
     // 发出mutation的同时直接设置本地数据
     queryClient.setQueryData(['todos', newTodo.id], newTodo)
     return { previousTodo, newTodo }
   },
   //如果mutation失败,将数据恢复到snapshot的状态
   onError: (err, newTodo, context) => {
     queryClient.setQueryData(
       ['todos', context.newTodo.id],
       context.previousTodo
     )
   },
   onSettled: newTodo => {
     queryClient.invalidateQueries(['todos', newTodo.id])
   },
})

总结一下Optimistic Updates流程

OptimisticUpdate


Author: Maple
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source Maple !
  TOC