阅读 2047

用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。你可以从这里看到他们的区别

初识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还为我们做了如下工作:

  • 多个组件请求同一个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 (
     <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, 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全局状态管理方案可以更专注于前端中间状态处理

文章分类
前端
文章标签