随着 React 的版本更新,内置 Hooks 越来越多了。很多 Hook 可能你压根都没听说过。但是 useEffect 这个老牌 Hook,相信每个用 React 的同学应该熟悉。
不优雅的 useEffect
不过对很多刚接触 React 的人来说,使用 useEffect 非常容易出现无限渲染循环的问题。
比如这样写:
const [count, setCount] = useState(0) useEffect(() => { setCount(count+1) })
原因是,useEffect 如果不传递第二个参数,它就会在组件每次渲染后执行。而 setCount 将会导致组件渲染。所以就会出现无限渲染循环的问题。
为了防止这个问题,我们可以给它设置第二个依赖项。依赖项是一个数组,可以传递多个引用,当其中的任意一个引用发生变化时,都会触发 effect。
const [count, setCount] = useState(0) useEffect(() => { setCount(count+1) }, [])
所以我们通常都需要设置第二个依赖项来防止意想不到的事情发生。
useEffect 的设计被很多人诟病,当然也有人很喜欢它。但我认为 useEffect 在设计上确实有些草率了。
严格模式怎么办?
useEffect 可以做很多事,最常见的事情就是发起 API 调用。像下面这样:
useEffect(() => { fetch('/xxx') }, [])
React 中有严格模式,如果开启了严格模式,那么 useEffect 将会被触发两次。
这么做的好处是提醒我们,这个组件是具有副作用的。
但是对应的,我们会发起两次 API 请求。这显然是不合适的。
我知道很多人不喜欢 React 严格模式,他们会选择直接关闭它。但是如果我就是想要严格模式带来的其他好处,该怎么办?
其实也很简单,我们只需要用一个在组件每次重新渲染时不会影响的东西来记录是否被调用过就可以了。很容易就可以想到 useRef 这个东西。
所以我们可以自己实现一个在严格模式下保证 useEffect 只运行一次的 Hook。
function useEffectOnce(fn: () => void) { const canCall = useRef(false) useEffect(() => { if(canCall.current) { fn() } return () => { canCall.current = true } }, []) }
这样就解决这个问题,但是它并不优雅。
性能并不好
如果在 useEffect 中发起 API 调用,那么 API 的调用将会在组件渲染完成才开始。
整个流程如下:
这个流程肯定不是最好的,因为渲染 UI 总归是要花费一些事件,尽管可能很短暂,你或许认为它无关紧要。但是性能问题往往就是积少成多导致的。
我们到底该怎么办?
对于这个问题,TanStack Query 提供了 useQuery Hook。
利用它,我们可以在组件开始渲染时同时开始 API 调用。
流程就像下面这样:
下面是一个使用 useEffect 的代码示例。
useEffect(() => { try { setLoading(true)(async () => { const data = await (await fetch('/data')).json(); setData(data); })(); } catch (error) { setError(error); } finally { setLoading(false); } }, []);
可以看到,我们除了需要处理数据以外,还要处理加载状态和异常状态。
如果使用 useQuery 会怎么样呢?
const { status, data, error, isFetching } = useQuery( ['data'], async () => { const data = await ( await fetch('/data') ).json() return data } )
它会帮我们处理好加载状态、错误状态和查询结果数据的更新。
如果我们想重新运行或者终止这个 API 调用也非常简单。
只需要调用 useQuery 对应的 API 就可以了。
queryClient.invalidateQueries(['data'])
除了 useQuery,我们还可以使用 SWR,它也是一个非常棒的库。
关于 SWR,我还有一篇单独介绍它的文章。
总结
在 useEffect 中进行 API 调用很容易出错,并且性能也不好。最好的方式就是放弃这种用法,直接采用类似 useQuery 或者 SWR 这类库,可以让我们更好的进行 API 调用。