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

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

  • 用户交互的中间状态
  • 服务端状态

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

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

用户交互的中间状态

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

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

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

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

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

服务端状态

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

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. 「缓存」 的性质不同于 「状态」

不同于交互的中间状态,服务端状态更应被归类为 「缓存」 ,他有如下性质:

  • 通常以 「异步」 的形式请求、更新
  • 「状态」 由请求的数据源控制,不由前端控制
  • 「状态」 可以由不同组件共享

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

  • 缓存失效
  • 缓存更新

Redux 一把梭固然方便。但是,区别对待不同类型 「状态」 能让项目更可控。

这里,推荐使用 React-Query 管理服务端状态。

另一个可选方案是 SWR [1]。你可以从 这里 [2]看到他们的区别

初识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 
loading
; } return (
    {data.map(user =>
  • {user.name}
  • )}
) }

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

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

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

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

query

数据的 CRUD 由2个 hook 处理:

useQuery
useMutation

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

import { useQuery, queryCache } from 'react-query';

 function App() {
   const {data, isLoading, isError} = useQuery('userData', () => axios.get('/api/user'));
   // 新增用户
   const {mutate} = useMutation(data => axios.post('/api/user', data));
 
   return (
     
    {data.map(user =>
  • {user.name}
  • )}
) }

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

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

import { useQuery, queryCache } from 'react-query';

function App() {
  // ...
  const {mutate} = useMutation(userData => axios.post('/api/user', userData), {
    onSuccess: () => {
      queryCache.invalidateQueries('userData')
    }  
  })
  
  // ...
}

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

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

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

总结

通过使用 React-Query (或 SWR )这样的数据请求库,可以将服务端状态从全局状态中解放出来。

这为我们带来很多好处:

  • 使用通用的 hook 处理请求中间状态
  • 多余请求合并
  • 针对缓存的更新/失效策略
  • Redux「全局状态管理方案」 可以更专注于 「前端中间状态」 处理

参考资料

[1]

SWR: https://swr.vercel.app/

[2]

这里: https://react-query.tanstack.com/comparison