如何使用React-query避免同一查询被多次调用?
问题场景
我在React应用中用React Query写了基于useQuery的useLogin和useLogout两个自定义Hook:
export const useLogin = (input?: LoginWithEmailPassword) => { const { data, isLoading, isSuccess, error } = useQuery( ["loginWithEmailPassword"], () => loginWithEmailPassword(input), { enabled: !!input, } ); return { data: data?.data, isLoading, isSuccess, error }; }; export const useLogout = (accessToken?: string) => { const { isSuccess, isLoading, error } = useQuery( ["logout"], () => logout(accessToken), { enabled: !!accessToken, } ); return { isSuccess, isLoading, error }; };
在AuthProvider组件里使用这两个Hook,通过维护loginInput和logoutInput状态来触发查询:
// AuthProvider.tsx const [loginInput, setLoginInput] = useState<LoginWithEmailPassword>(); const [logoutInput, setLogoutInput] = useState<string | undefined>(); const { data, isSuccess: isLoginSuccess, isLoading } = useLogin(loginInput); const { ...logoutProps } = useLogout(logoutInput); const login = (input: LoginWithEmailPassword) => { setLoginInput(input); }; const logout = () => { setLogoutInput(data?.accessToken); };
但遇到一个问题:用户点击登录后,loginInput状态被设置,useLogin会执行查询,但因为loginInput状态一直存在,组件每次重渲染(比如点击登出时)useLogin都会重新执行一遍。我想找最佳解决办法,要是React Query有类似Apollo的useLazyQuery就更直观了。目前想到一个临时方案:登录成功后把loginInput重置为undefined:
useEffect(()=>{ if(isLoginSuccess && !isLoading && !!data) setLoginInput(undefined) },[isLoginSuccess])
最佳解决方案
1. 改用useMutation处理登录/登出(推荐)
登录和登出属于修改服务器状态的操作,React Query中这类场景应该用useMutation而不是useQuery,它天生就是用来处理一次性的写操作,不需要依赖状态来控制执行,直接手动调用mutate触发即可:
重写登录/登出Hook
// useLogin.ts export const useLogin = () => { const mutation = useMutation({ mutationFn: (input: LoginWithEmailPassword) => loginWithEmailPassword(input), // 可选:添加成功/失败回调 onSuccess: (data) => { // 登录成功后的逻辑,比如存储token }, onError: (error) => { // 处理登录错误 } }); return { login: mutation.mutate, data: mutation.data?.data, isLoading: mutation.isLoading, isSuccess: mutation.isSuccess, error: mutation.error }; }; // useLogout.ts export const useLogout = () => { const mutation = useMutation({ mutationFn: (accessToken: string) => logout(accessToken), onSuccess: () => { // 登出成功后的逻辑,比如清除本地token } }); return { logout: mutation.mutate, isLoading: mutation.isLoading, isSuccess: mutation.isSuccess, error: mutation.error }; };
在AuthProvider中使用
// AuthProvider.tsx const { login, data, isLoading, isSuccess: isLoginSuccess, error } = useLogin(); const { logout: logoutMutate, isLoading: isLogoutLoading } = useLogout(); const handleLogin = (input: LoginWithEmailPassword) => { login(input); }; const handleLogout = () => { if (data?.accessToken) { logoutMutate(data.accessToken); } };
这样就完全不需要维护loginInput和logoutInput状态,点击按钮直接调用login/logoutMutate即可触发操作,不会出现重复执行的问题。
2. 模拟useLazyQuery的效果(如果坚持用useQuery)
如果一定要用useQuery实现懒加载触发的效果,可以通过refetch方法手动触发查询,同时设置enabled: false让查询默认不执行:
修改useLogin Hook
export const useLogin = () => { const { data, isLoading, isSuccess, error, refetch } = useQuery( ["loginWithEmailPassword"], ({ queryKey, pageParam }) => loginWithEmailPassword(pageParam), { enabled: false, // 默认不执行 } ); // 封装带参数的触发函数 const triggerLogin = (input: LoginWithEmailPassword) => { return refetch(input); }; return { triggerLogin, data: data?.data, isLoading, isSuccess, error }; };
在AuthProvider中使用
const { triggerLogin, data, isLoading, isSuccess: isLoginSuccess } = useLogin(); const handleLogin = (input: LoginWithEmailPassword) => { triggerLogin(input); };
这种方式下,查询只会在调用triggerLogin时执行,不会随组件重渲染重复运行。
3. 优化你的临时方案
如果暂时不想修改Hook,也可以优化你原来的重置逻辑,确保只在登录成功后重置状态,避免不必要的重渲染:
useEffect(() => { // 确保登录成功且数据存在时才重置 if (isLoginSuccess && !isLoading && data) { setLoginInput(undefined); } }, [isLoginSuccess, isLoading, data]);
注意要把isLoading和data也加入依赖数组,避免逻辑触发时机错误。
内容的提问来源于stack exchange,提问作者webber

