Vue Query 实例解析:轻松掌握核心用法

前置假设:

  • 你已经安装了 @tanstack/vue-query
  • 你已经在项目的入口文件(如 main.ts)中设置好了 QueryClientVueQueryPlugin(如之前介绍中所示)。
  • 你有一个发送 HTTP 请求的方式(这里以 axios 为例,但 fetch 或其他库同样适用)。假设 axios 已配置好,并在需要的地方导入。

示例 1:获取并展示列表数据 (例如:待办事项列表)

这是最基础的用法。

vue 复制代码
<script setup lang="ts">
import { useQuery } from '@tanstack/vue-query';
import axios from 'axios'; // 假设 axios 已配置

// 定义获取待办事项列表的函数
const fetchTodos = async () => {
  console.log('发起请求获取 Todos...'); // 方便观察请求时机
  const response = await axios.get('/api/todos');
  return response.data; // Vue Query 需要 Promise 返回实际数据
};

// 使用 useQuery Hook
const {
  data: todos,    // 响应式的 ref,持有获取到的数据 (或 undefined)
  status,       // 查询状态: 'pending', 'error', 'success'
  isLoading,   // 响应式的 ref: 初始加载时为 true (v5 前常用)
  isPending,    // 响应式的 ref: 初始加载时为 true (v5 推荐)
  isFetching,  // 响应式的 ref: 任何获取过程中都为 true (包括后台刷新)
  isError,     // 响应式的 ref: 获取失败时为 true
  error,       // 响应式的 ref: 持有错误对象
  refetch       // 手动触发重新获取的函数
} = useQuery({
  queryKey: ['todos'], // 必需:此查询的唯一标识键 (数组或字符串)
  queryFn: fetchTodos,  // 必需:用于获取数据的异步函数
  // 可选配置:如果数据不经常变动,可以增加 staleTime
  // staleTime: 5 * 60 * 1000, // 数据在 5 分钟内视为"新鲜",不会因窗口聚焦等自动刷新
});
</script>

<template>
  <div>
    <h2>待办事项列表</h2>

    <!-- 加载状态 -->
    <div v-if="isPending">正在加载待办事项...</div>

    <!-- 错误状态 -->
    <div v-else-if="isError">发生错误: {{ error?.message }}</div>

    <!-- 成功状态 -->
    <ul v-else-if="todos">
      <li v-for="todo in todos" :key="todo.id">
        {{ todo.title }} - {{ todo.completed ? '已完成' : '待办' }}
      </li>
    </ul>

    <!-- 无数据 -->
    <div v-else>暂无待办事项。</div>

    <!-- 后台刷新指示器 -->
    <div v-if="isFetching && !isPending" class="fetching-indicator">
      正在后台更新...
    </div>

    <!-- 手动刷新按钮 -->
    <button @click="refetch()" :disabled="isFetching">
      {{ isFetching ? '刷新中...' : '手动刷新' }}
    </button>
  </div>
</template>

<style scoped>
.fetching-indicator {
  position: fixed;
  bottom: 1rem;
  right: 1rem;
  background-color: lightblue;
  padding: 0.5rem;
  border-radius: 4px;
  font-size: 0.8em;
}
</style>

说明:

  • queryKey: ['todos']: 唯一标识了这个列表数据。如果其他地方也用这个 key 调用 useQuery,它们会共享缓存和状态。
  • queryFn: fetchTodos: 告诉 Vue Query 如何获取数据。
  • Vue Query 自动管理了 isPending, isError, data 等响应式状态,可以直接在模板中使用。
  • isFetching 表示任何获取过程,包括 Stale-While-Revalidate 或窗口聚焦触发的后台刷新。

示例 2:根据路由参数获取项目详情

这很常见,比如 /todos/1 显示 ID 为 1 的待办事项。

vue 复制代码
<script setup lang="ts">
import { useQuery } from '@tanstack/vue-query';
import { useRoute } from 'vue-router';
import { computed, watch } from 'vue';
import axios from 'axios';

const route = useRoute();

// 使用 computed 来响应式地获取路由参数 ID
const todoId = computed(() => route.params.id as string | undefined);

// 获取函数现在需要接收 ID
const fetchTodoById = async (id: string) => {
  // 注意:最好处理 id 不存在或无效的情况
  if (!id || Number.isNaN(parseInt(id, 10))) {
     // 可以返回 null, undefined, 或抛出错误,取决于你希望如何处理
     console.warn('无效的 Todo ID:', id);
     return null;
     // 或者 throw new Error('无效的 ID');
  }
  console.log(`发起请求获取 Todo ID: ${id}...`);
  const response = await axios.get(`/api/todos/${id}`);
  return response.data;
};

const {
  data: todo,
  isLoading, // 还是用 isLoading 吧,更清晰区分首次加载
  isError,
  error,
  status, // 可以直接用 status 判断更细致的状态
  // isFetching, // 如果需要后台刷新指示器也可以用
} = useQuery({
  // Query Key 现在包含动态 ID。
  // 当 todoId.value 变化时,Vue Query 会自动识别为不同的查询并获取数据!
  queryKey: ['todo', todoId],
  // queryFn 会接收一个包含 queryKey 的上下文对象,但直接用闭包访问 todoId 更方便
  queryFn: () => fetchTodoById(todoId.value!), // 传递当前的 ID
  // 只有当 ID 存在时才自动执行查询
  enabled: computed(() => !!todoId.value && !Number.isNaN(parseInt(todoId.value, 10))),
});

// (可选) 监听 status 或 error 变化来执行副作用,如提示
watch(status, (newStatus) => {
  console.log('Todo query status changed:', newStatus);
});
</script>

<template>
  <div>
    <h2>待办事项详情</h2>
    <div v-if="isLoading">正在加载详情...</div>
    <div v-else-if="isError">错误: {{ error?.message }}</div>
    <div v-else-if="todo">
      <h3>{{ todo.title }}</h3>
      <p>状态: {{ todo.completed ? '已完成' : '待办' }}</p>
      <p>ID: {{ todo.id }}</p>
    </div>
    <!-- 处理 fetchTodoById 返回 null 或 enabled 为 false 的情况 -->
    <div v-else-if="!todoId">请提供有效的待办事项 ID。</div>
    <div v-else>未找到该待办事项。</div>
  </div>
</template>

说明:

  • queryKey: ['todo', todoId]: 关键在于将响应式的 todoId (一个 computed ref) 放入 queryKey。当路由变化导致 todoId.value 更新时,Vue Query 会自动检测到 queryKey 的变化,并为新的 ID 获取数据(或使用该 ID 的缓存)。
  • enabled: computed(() => !!todoId.value && ...): 这是一个非常有用的选项,它确保只有当 todoId 有效时,查询才会自动执行,避免了在 ID 无效时发起不必要的 API 调用。

示例 3:添加项目 (Mutation + 缓存失效)

执行一个会改变数据的操作(如 POST 请求),然后更新列表视图。

vue 复制代码
<script setup lang="ts">
import { ref } from 'vue';
import { useMutation, useQueryClient } from '@tanstack/vue-query';
import axios from 'axios';

const queryClient = useQueryClient(); // 获取 QueryClient 实例,用于与缓存交互
const newTodoTitle = ref('');

// 定义执行变更的异步函数 (添加新的 todo)
const addTodoMutationFn = async (title: string) => {
  console.log('发起请求添加 Todo:', title);
  const response = await axios.post('/api/todos', { title, completed: false });
  return response.data;
};

// 使用 useMutation Hook
const {
  mutate: addTodo, // 调用此函数来触发变更操作 (不返回 Promise)
  // mutateAsync: addTodoAsync, // 如果需要 Promise,用这个
  status: addStatus,       // 变更的状态: 'idle', 'pending', 'error', 'success'
  isPending: isAddingTodo, // 是否正在执行变更
  isError: isAddError,
  error: addError,
  data: addedTodoData, // 变更成功后 mutationFn 返回的数据
} = useMutation({
  mutationFn: addTodoMutationFn,
  // --- 关键:成功回调 ---
  onSuccess: (newData, variables, context) => {
    // 变更成功!最常用的策略:使 'todos' 查询失效。
    // 这会告诉 Vue Query 与 ['todos'] 键关联的数据现在"不新鲜"了。
    // 所有活跃的 useQuery(['todos']) 实例会自动在后台重新获取数据。
    console.log('待办事项添加成功:', newData, ',原始变量:', variables);

    // 使列表查询失效
    queryClient.invalidateQueries({ queryKey: ['todos'] });

    // --- 另一种选择:手动更新缓存 (更即时,但可能更复杂) ---
    // 如果你想避免重新请求列表,可以手动更新缓存:
    // queryClient.setQueryData(['todos'], (oldData: any[] | undefined) => {
    //   // 确保 oldData 是数组
    //   const currentData = Array.isArray(oldData) ? oldData : [];
    //   return [...currentData, newData]; // 返回更新后的数组
    // });

    newTodoTitle.value = ''; // 清空输入框
  },
  // --- 错误处理 ---
  onError: (error, variables, context) => {
    console.error('添加待办事项失败:', error, ',提交的数据:', variables);
    // 可以在这里显示错误提示给用户
    alert(`添加失败: ${error.message}`);
  },
  // onSettled: (data, error, variables, context) => {
  //   // 无论成功或失败都会执行
  //   console.log('添加操作完成');
  // }
});

// 提交表单的处理函数
const submitNewTodo = () => {
  const title = newTodoTitle.value.trim();
  if (title) {
    addTodo(title); // 调用 mutate 函数,传入需要的数据
  }
};
</script>

<template>
  <div>
    <h3>添加新的待办事项</h3>
    <form @submit.prevent="submitNewTodo">
      <input type="text" v-model="newTodoTitle" placeholder="输入待办事项标题" :disabled="isAddingTodo" />
      <button type="submit" :disabled="isAddingTodo">
        {{ isAddingTodo ? '添加中...' : '添加' }}
      </button>
    </form>
    <div v-if="isAddError" style="color: red;">
      添加错误: {{ addError?.message }}
    </div>
    <!-- 列表组件 (如示例 1) 会因为 ['todos'] 查询失效并重新获取而自动更新 -->
  </div>
</template>

说明:

  • useMutation: 用于执行会改变服务器数据的操作。
  • mutationFn: 执行实际的 POST/PUT/DELETE 请求。
  • mutate: 调用这个函数来启动变更过程,并传入需要提交的数据。
  • onSuccess: 这是核心 。变更成功后:
    • queryClient.invalidateQueries({ queryKey: ['todos'] }): 这是最常用也通常推荐的方式。它告诉 Vue Query ['todos'] 这个 key 的数据可能过期了。任何正在使用这个 key 的 useQuery 都会自动安排一次后台刷新,从而让列表 UI 更新。简单、可靠。
    • 注释掉的 setQueryData 展示了手动更新缓存的方式,可以提供更即时的 UI 反馈(无需等待刷新),但需要你精确地维护缓存数据结构。

示例 4:删除项目 (Mutation + 乐观更新)

乐观更新通过在服务器确认前就更新 UI,来提供更流畅的用户体验。

vue 复制代码
<script setup lang="ts">
import { useMutation, useQueryClient } from '@tanstack/vue-query';
import axios from 'axios';

// 假设这是 TodoItem 组件的一部分,接收一个 todo 对象作为 prop
interface Todo {
  id: number;
  title: string;
  completed: boolean;
}
const props = defineProps<{ todo: Todo }>();

const queryClient = useQueryClient();

// 定义删除操作的函数
const deleteTodoMutationFn = async (id: number) => {
  console.log(`发起请求删除 Todo ID: ${id}...`);
  // DELETE 请求通常不返回内容,或者只返回状态码
  await axios.delete(`/api/todos/${id}`);
};

// 使用 useMutation,配置乐观更新
const { mutate: deleteTodo, isPending: isDeleting } = useMutation({
  mutationFn: deleteTodoMutationFn,
  // --- 乐观更新逻辑 ---
  onMutate: async (idToDelete) => {
    console.log(`乐观更新:尝试删除 ID: ${idToDelete}`);
    // 1. 取消可能正在进行的针对 'todos' 列表的重新获取请求
    //    这可以防止它们覆盖我们的乐观更新。
    await queryClient.cancelQueries({ queryKey: ['todos'] });

    // 2. 获取当前的 'todos' 列表缓存快照
    const previousTodos = queryClient.getQueryData<Todo[]>(['todos']);

    // 3. 乐观地从缓存中移除这一项
    if (previousTodos) {
      queryClient.setQueryData<Todo[]>(
        ['todos'], // 要更新的缓存 key
        // 更新函数:接收旧数据,返回新数据
        (old) => old?.filter(todo => todo.id !== idToDelete) ?? []
      );
      console.log('乐观更新:UI 上的列表已更新');
    } else {
      console.log('乐观更新:未找到缓存数据,无法进行乐观更新');
    }


    // 4. 返回一个包含快照的上下文对象,以便在出错时回滚
    return { previousTodos };
  },
  // --- 错误处理:回滚 ---
  onError: (err, idToDelete, context) => {
    console.error("删除失败,需要回滚:", err);
    // 如果 onMutate 中成功获取了快照,则用它来恢复缓存
    if (context?.previousTodos) {
      console.log('回滚 UI...');
      queryClient.setQueryData<Todo[]>(['todos'], context.previousTodos);
    }
    // 向用户显示错误信息
    alert(`删除失败: ${err.message}`);
  },
  // --- 最终处理:确保一致性 ---
  onSettled: (data, error, idToDelete, context) => {
    // 无论删除成功还是失败,最终都要重新从服务器获取一次 'todos' 列表
    // 来确保客户端状态与服务器状态最终一致。
    console.log(`删除操作完成 (成功或失败),重新验证 'todos' 列表`);
    queryClient.invalidateQueries({ queryKey: ['todos'] });
  },
});

// 处理删除按钮点击
const handleDelete = () => {
  // 可以加一个确认框
  if (confirm(`确定要删除 "${props.todo.title}" 吗?`)) {
    deleteTodo(props.todo.id); // 调用 mutate 触发删除
  }
};
</script>

<template>
  <li>
    {{ todo.title }}
    <button @click="handleDelete" :disabled="isDeleting" style="margin-left: 10px;">
      {{ isDeleting ? '删除中...' : '删除' }}
    </button>
  </li>
</template>

说明:

  • 乐观更新 (Optimistic Update): 核心在于 onMutate。UI 会在用户点击删除后立即移除该项,感觉非常流畅。
  • onMutate:
    • cancelQueries: 防止后台刷新覆盖乐观更新。
    • getQueryData: 获取当前缓存。
    • setQueryData: 立即修改缓存,移除对应项。
    • 返回 context: 保存原始数据用于回滚。
  • onError: 如果服务器删除失败,使用 context.previousTodos 通过 setQueryData 将缓存恢复到原始状态。
  • onSettled: 极其重要 。无论成功或失败,最后都要调用 invalidateQueries({ queryKey: ['todos'] })。这确保了客户端最终会与服务器的真实状态同步,修正了乐观更新可能带来的(暂时性)不一致。

这些示例覆盖了 Vue Query 最常用的一些场景。关键在于理解 queryKey 的作用、useQueryuseMutation 的基本用法,以及如何利用 QueryClient 的方法(尤其是 invalidateQueriessetQueryData)来管理缓存和状态更新。根据你的具体需求调整这些模式即可。

相关推荐
蘑菇头爱平底锅6 分钟前
数字孪生-DTS-孪创城市-湖区分布
前端·数据可视化
Enti7c1 小时前
css定位
前端·css
zy0101012 小时前
useEffect
开发语言·前端·javascript·react·useeffect
@PHARAOH2 小时前
WHAT - React 进一步学习推荐
前端·学习·react.js
kovlistudio2 小时前
红宝书第四十讲:React 核心概念:组件化 & 虚拟 DOM 简单教程
开发语言·前端·javascript·学习·react.js·前端框架
巴巴_羊2 小时前
React Redux
开发语言·前端·javascript
Mintopia2 小时前
Node.js 中的this
前端·javascript·node.js
Mike_jia2 小时前
一篇文章带你了解一款强大的开源跨平台远程桌面管理工具---XPipe
前端·开源
._Ha!n.3 小时前
React基础知识一
前端·react.js
Mintopia3 小时前
深入理解 Three.js 中 Shader 的使用及示例
前端·javascript·three.js