按照来源,前端有两类状态需要管理:
-
用户交互的中间状态
-
服务端状态
在陈年的老项目中,通常用Redux
、Mobx
这样的全局状态管理方案无差别对待他们。
事实上,他们有很大区别:
用户交互的中间状态
比如组件的isLoading
、isOpen
,这类状态的特点是:
-
以同步的形式更新
-
状态完全由前端控制
-
状态比较独立(不同的组件拥有各自的
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个坏处:
- 需要重复处理请求中间状态
为了让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
}
这类通用的中间状态处理逻辑可能在不同组件中重复写很多次。
- 缓存的性质不同于状态
不同于交互的中间状态,服务端状态更应被归类为缓存,他有如下性质:
-
通常以异步的形式请求、更新
-
状态由请求的数据源控制,不由前端控制
-
状态可以由不同组件共享
作为可以由不同组件共享的缓存,还需要考虑更多问题,比如:
-
缓存失效
-
缓存更新
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
封装了完整的请求中间状态(isLoading
、isError
...)。
不仅如此,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-Query
,userData 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
等全局状态管理方案可以更专注于前端中间状态处理