最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

React Query + REST API 最佳實踐

 更新時間:2026年06月03日 08:18:46   作者:ZengLiangYi  
本文介紹了如何利用React Query構(gòu)建React應(yīng)用的REST API數(shù)據(jù)層,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧

為什么選 React Query

在沒有 React Query 之前,常見的做法是 useState + useEffect + fetch

const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
  let cancelled = false;
  fetch('/api/notes')
    .then(res => res.json())
    .then(json => { if (!cancelled) setData(json.data); })
    .catch(err => { if (!cancelled) setError(err); })
    .finally(() => { if (!cancelled) setLoading(false); });
  return () => { cancelled = true; };
}, []);

這段代碼有三個明顯的問題:

  1. 緩存缺失 -- 切換頁面再切回來,數(shù)據(jù)重新請求,白屏閃爍。
  2. 狀態(tài)同步困難 -- 多個組件用同一份數(shù)據(jù)時,需要手動傳遞或用全局狀態(tài)管理。
  3. 競態(tài)風(fēng)險 -- 快速切換頁面,舊請求可能覆蓋新請求的結(jié)果。

React Query 解決了以上所有問題,同時提供了后臺刷新、樂觀更新、分頁、輪詢等開箱即用的能力。

QueryClient 配置

ChatCrystal 在應(yīng)用入口 App.tsx 中創(chuàng)建 QueryClient,并通過 QueryClientProvider 注入整個應(yīng)用:

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 30_000,  // 30 秒內(nèi)認(rèn)為數(shù)據(jù)是新鮮的,不重新請求
      retry: 1,           // 請求失敗只重試 1 次
    },
  },
});

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      {/* 路由、主題等組件 */}
    </QueryClientProvider>
  );
}

幾個關(guān)鍵參數(shù)的含義:

參數(shù)默認(rèn)值說明
staleTime0數(shù)據(jù)被視為"過期"的時間。設(shè)為 30 秒意味著 30 秒內(nèi)重新掛載組件不會觸發(fā)網(wǎng)絡(luò)請求
retry3失敗后的重試次數(shù)。API 通常重試 1 次就夠了
gcTime (v5)5 分鐘數(shù)據(jù)不再被任何組件使用后,在緩存中保留的時間

ChatCrystal 把 staleTime 設(shè)為 30 秒,這對內(nèi)部工具類應(yīng)用很合理 -- 數(shù)據(jù)不需要實時,但也不能太舊。

API 層設(shè)計

ChatCrystal 在 lib/api.ts 中封裝了一個通用的請求函數(shù):

const BASE = "/api";

async function request<T>(path: string, options?: RequestInit): Promise<T> {
  const headers: Record<string, string> = {};
  if (options?.body) {
    headers["Content-Type"] = "application/json";
  }
  const res = await fetch(`${BASE}${path}`, { headers, ...options });
  if (!res.ok) {
    const body = await res.json().catch(() => ({}));
    throw new Error(body.error || `Request failed: ${res.status}`);
  }
  const json = await res.json();
  if (!json.success) throw new Error(json.error || "Unknown error");
  return json.data;
}

這里的約定是:后端統(tǒng)一返回 { success: boolean, data: T, error?: string } 的格式,request 函數(shù)負(fù)責(zé)解包。如果有同學(xué)在設(shè)計自己的 API 層,建議也采用類似的統(tǒng)一響應(yīng)格式。

開發(fā)環(huán)境下的跨域問題由 Vite 的代理解決,不需要在前端處理 CORS:

// client/vite.config.ts
server: {
  port: 13721,
  proxy: {
    '/api': 'http://localhost:3721',
  },
},

所有 /api 開頭的請求會被轉(zhuǎn)發(fā)到后端的 3721 端口。

自定義 Hook -- 將查詢邏輯封裝為可復(fù)用單元

ChatCrystal 不在頁面組件中直接寫 useQuery,而是把每個查詢封裝成自定義 Hook。這有三個好處:頁面組件更簡潔、查詢邏輯可復(fù)用、queryKey 管理集中化。

// hooks/use-notes.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from '@/lib/api';

// 列表查詢 -- 帶篩選和分頁參數(shù)
export function useNotes(params?: {
  tag?: string;
  search?: string;
  offset?: number;
  limit?: number;
}) {
  return useQuery({
    queryKey: ['notes', params],
    queryFn: () => api.getNotes(params),
  });
}

// 單條詳情查詢 -- 啟用條件控制
export function useNote(id: number) {
  return useQuery({
    queryKey: ['note', id],
    queryFn: () => api.getNote(id),
    enabled: id > 0,
  });
}

注意 useNote 中的 enabled: id > 0。當(dāng)路由參數(shù)還沒準(zhǔn)備好時(比如 id 還是 undefined),查詢不會執(zhí)行,避免無意義的請求。

queryKey 的設(shè)計原則

React Query 用 queryKey 做緩存的鍵。設(shè)計原則是:

  • 列表和詳情用不同的 key:['notes', params] 和 ['note', id] 是兩個獨立的緩存條目。
  • 參數(shù)變化自動觸發(fā)重新請求:useNotes({ tag: 'react' }) 和 useNotes({ tag: 'vue' }) 各自獨立緩存。
  • 數(shù)組中的對象會被深度比較:所以 params 對象中的字段順序不影響緩存匹配。

useMutation -- 寫操作與緩存失效

查詢是讀,mutation 是寫。寫操作完成后,緩存中的舊數(shù)據(jù)需要更新。ChatCrystal 的做法是:在 onSuccess 中批量失效相關(guān) query。

export function useDeleteNote() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: ({ id, reason, comment }: {
      id: number;
      reason: ExperienceReviewReason;
      comment?: string;
    }) => api.deleteNote(id, { reason, comment, source: 'web' }),
    onSuccess: (data, variables) => {
      // 刪除一條筆記后,需要刷新所有相關(guān)數(shù)據(jù)
      queryClient.invalidateQueries({ queryKey: ['notes'] });
      queryClient.invalidateQueries({ queryKey: ['note', variables.id] });
      queryClient.invalidateQueries({ queryKey: ['tags'] });
      queryClient.invalidateQueries({ queryKey: ['note-relations'] });
      queryClient.invalidateQueries({ queryKey: ['relation-graph'] });
      queryClient.invalidateQueries({ queryKey: ['conversations'] });
      queryClient.invalidateQueries({ queryKey: ['conversation', data.conversationId] });
      queryClient.invalidateQueries({ queryKey: ['status'] });
    },
  });
}

invalidateQueries 不會立即重新請求。它只是把匹配的緩存標(biāo)記為"過期",下次組件掛載時才會重新獲取。如果當(dāng)前有組件正在使用這些數(shù)據(jù),React Query 會立即在后臺發(fā)起一次刷新。

典型的 mutation 流程

以"總結(jié)對話"為例,這是一個異步操作,涉及隊列系統(tǒng):

export function useSummarize() {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: (conversationId: string) => api.summarize(conversationId),
    onSuccess: (_data, conversationId) => {
      queryClient.invalidateQueries({ queryKey: ['queue-status'] });
      queryClient.invalidateQueries({ queryKey: ['conversation', conversationId] });
      queryClient.invalidateQueries({ queryKey: ['conversations'] });
      queryClient.invalidateQueries({ queryKey: ['notes'] });
      queryClient.invalidateQueries({ queryKey: ['status'] });
    },
  });
}

點擊"生成摘要"按鈕后:

  1. mutationFn 發(fā)送 POST 請求,將任務(wù)加入隊列。
  2. onSuccess 失效隊列狀態(tài)的緩存,StatusBar 開始輪詢進度。
  3. 隊列完成后,相關(guān)頁面的數(shù)據(jù)會在下次訪問時自動刷新。

輪詢 -- 處理異步任務(wù)狀態(tài)

ChatCrystal 有一個任務(wù)隊列系統(tǒng),需要實時顯示進度。React Query 的 refetchInterval 可以實現(xiàn)智能輪詢:

export function useQueueTasks() {
  return useQuery({
    queryKey: ['queue-status'],
    queryFn: () => api.getQueueStatus(),
    refetchInterval: (query) => {
      const data = query.state.data as QueueSnapshot | undefined;
      // 有活躍任務(wù)時 2 秒輪詢一次,否則 10 秒一次
      return data && data.active > 0 ? 2000 : 10000;
    },
  });
}

refetchInterval 支持傳入函數(shù),可以根據(jù)當(dāng)前數(shù)據(jù)動態(tài)調(diào)整輪詢頻率。這個技巧很實用 -- 任務(wù)進行中時高頻輪詢以獲取實時進度,空閑時降低頻率以節(jié)省資源。

Dashboard 頁面也用了一個簡單的 30 秒輪詢來保持狀態(tài)數(shù)據(jù)新鮮(這個 hook 定義在 hooks/use-conversations.ts 中):

export function useStatus() {
  return useQuery({
    queryKey: ['status'],
    queryFn: () => api.getStatus(),
    refetchInterval: 30_000,
  });
}

加載與錯誤狀態(tài)處理

React Query 的 useQuery 返回三個關(guān)鍵狀態(tài)字段:isLoading、isErrordata。頁面組件根據(jù)這些字段渲染不同內(nèi)容。

// 頁面組件中的典型模式
export function Notes() {
  const { data, isLoading } = useNotes({ /* params */ });

  if (isLoading) {
    return <p className="text-muted text-sm">加載中...</p>;
  }

  if (!data || data.items.length === 0) {
    return <div className="text-center py-12">暫無筆記</div>;
  }

  return (
    <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
      {data.items.map(note => (
        <NoteCard key={note.id} note={note} />
      ))}
    </div>
  );
}

對于 mutation 的錯誤,ChatCrystal 在搜索頁面展示了典型的錯誤處理:

const search = useMutation({
  mutationFn: (q: string) => api.search(q, 10, expand),
});

// 在 JSX 中
{search.isError && (
  <p className="text-error text-sm">{search.error.message}</p>
)}

mutation 的按鈕也需要根據(jù) isPending 狀態(tài)禁用,防止重復(fù)提交:

<button
  onClick={handleSearch}
  disabled={search.isPending || !query.trim()}
>
  {search.isPending ? <Loader2 className="animate-spin" /> : '搜索'}
</button>

分頁實現(xiàn)

ChatCrystal 使用傳統(tǒng)的偏移量分頁,每頁 20 條:

export function Notes() {
  const [page, setPage] = useState(0);
  const limit = 20;

  const { data, isLoading } = useNotes({
    search: search || undefined,
    tag: activeTag || undefined,
    offset: page * limit,
    limit,
  });

  // 翻頁時重置到第一頁
  const handleSearch = (value: string) => {
    setSearch(value);
    setPage(0);
  };

  // 分頁控件
  {data && data.total > limit && (
    <div className="flex items-center justify-center gap-4 mt-4">
      <button disabled={page === 0} onClick={() => setPage(p => p - 1)}>
        上一頁
      </button>
      <span>{page + 1} / {Math.ceil(data.total / limit)}</span>
      <button disabled={(page + 1) * limit >= data.total} onClick={() => setPage(p => p + 1)}>
        下一頁
      </button>
    </div>
  )}
}

后端返回 { items, total, offset, limit } 的格式,前端根據(jù) total 計算總頁數(shù)。注意 offset 是 queryKey 的一部分,翻頁時 React Query 會自動為每一頁創(chuàng)建獨立的緩存條目 -- 這意味著回翻時不需要重新請求。

如果需要無限滾動(infinite scroll),React Query 還提供了 useInfiniteQuery,用 getNextPageParamfetchNextPage 實現(xiàn)自動加載下一頁。但 ChatCrystal 的數(shù)據(jù)量不大,傳統(tǒng)分頁更合適。

樂觀更新

React Query 支持樂觀更新(Optimistic Update),在 mutation 發(fā)送請求之前就更新 UI,讓操作感覺更流暢。ChatCrystal 目前沒有使用這個模式,但這是一個值得了解的最佳實踐。

// 樂觀更新示例(非 ChatCrystal 代碼,僅為教學(xué))
export function useUpdateNote() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (updated: Note) => api.updateNote(updated),
    onMutate: async (updated) => {
      // 1. 取消正在進行的查詢,避免覆蓋我們的樂觀更新
      await queryClient.cancelQueries({ queryKey: ['note', updated.id] });
      // 2. 快照當(dāng)前數(shù)據(jù),以便回滾
      const previous = queryClient.getQueryData(['note', updated.id]);
      // 3. 樂觀更新緩存
      queryClient.setQueryData(['note', updated.id], updated);
      return { previous };
    },
    onError: (_err, updated, context) => {
      // 4. 出錯時回滾到快照
      queryClient.setQueryData(['note', updated.id], context?.previous);
    },
    onSettled: (_data, _err, updated) => {
      // 5. 無論成功失敗,都重新獲取服務(wù)端數(shù)據(jù)
      queryClient.invalidateQueries({ queryKey: ['note', updated.id] });
    },
  });
}

這個模式的要點是:onMutate 中做樂觀更新和快照,onError 中回滾,onSettled 中同步服務(wù)端真實數(shù)據(jù)。

懶加載路由與 Suspense

ChatCrystal 的路由使用了 React 的 lazy + Suspense,結(jié)合 React Router v7:

const NotesPage = lazy(() =>
  import('@/pages/Notes.tsx').then(module => ({ default: module.Notes }))
);

// 路由定義
<Route path="/notes" element={
  <RouteSuspense>
    <NotesPage />
  </RouteSuspense>
} />

頁面組件按需加載,首屏只加載當(dāng)前路由需要的代碼。RouteSuspense 包裹了統(tǒng)一的加載提示。

總結(jié)

ChatCrystal 的數(shù)據(jù)層架構(gòu)可以歸納為三層:

  1. API 層lib/api.ts)-- 封裝 fetch,統(tǒng)一錯誤處理和響應(yīng)格式。
  2. Hook 層hooks/ 目錄)-- 每個業(yè)務(wù)域一個文件,用 useQuery / useMutation 封裝讀寫操作,管理 queryKey 和緩存失效策略。
  3. 頁面層pages/ 目錄)-- 只關(guān)心 UI 渲染,從 Hook 獲取數(shù)據(jù)和狀態(tài)。

這種分層的好處是:頁面組件不關(guān)心數(shù)據(jù)從哪里來,Hook 不關(guān)心數(shù)據(jù)怎么展示。當(dāng)后端 API 變化時,只需要修改 api.ts;當(dāng)緩存策略需要調(diào)整時,只需要修改對應(yīng)的 Hook。

如果你正在構(gòu)建一個需要頻繁與 REST API 交互的 React 應(yīng)用,React Query 是目前最好的選擇。它不只是一個數(shù)據(jù)獲取庫,更是一個完整的異步狀態(tài)管理方案。

到此這篇關(guān)于React Query + REST API 最佳實踐的文章就介紹到這了,更多相關(guān)React Query + REST API 內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • React中10種Hook的使用介紹

    React中10種Hook的使用介紹

    Hook 是 React 16.8 的新增特性,本文主要介紹了10種Hook的使用,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2021-11-11
  • 更強大的React 狀態(tài)管理庫Zustand使用詳解

    更強大的React 狀態(tài)管理庫Zustand使用詳解

    這篇文章主要為大家介紹了更強大的React 狀態(tài)管理庫Zustand使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-10-10
  • React中的stopPropagation和preventDefault實踐記錄

    React中的stopPropagation和preventDefault實踐記錄

    本文詳細(xì)介紹了事件冒泡、捕獲與React合成事件體系下的表現(xiàn)區(qū)別,包括事件傳播階段、處理方式、事件池機制等核心概念,對React?stopPropagation和preventDefault相關(guān)知識感興趣的朋友跟隨小編一起看看吧
    2025-11-11
  • React中useState的使用方法及注意事項

    React中useState的使用方法及注意事項

    useState通過在函數(shù)組件里調(diào)用它來給組件添加一些內(nèi)部state,下面這篇文章主要給大家介紹了關(guān)于React中useState的使用方法及注意事項的相關(guān)資料,文中通過實例代碼介紹的非常詳細(xì),需要的朋友可以參考下
    2022-08-08
  • react實現(xiàn)可播放的進度條

    react實現(xiàn)可播放的進度條

    這篇文章主要為大家詳細(xì)介紹了react實現(xiàn)可播放的進度條,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2022-03-03
  • react自定義實現(xiàn)狀態(tài)管理詳解

    react自定義實現(xiàn)狀態(tài)管理詳解

    這篇文章主要為大家詳細(xì)介紹了react如何自定義實現(xiàn)狀態(tài)管理,文中的示例代碼講解詳細(xì),具有一定的借鑒價值,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下
    2024-01-01
  • 淺談使用React.setState需要注意的三點

    淺談使用React.setState需要注意的三點

    本篇文章主要介紹了淺談使用React.setState需要注意的三點,提出了三點對 React 新手來說是很容易忽略的地方,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2017-12-12
  • react實現(xiàn)頭部導(dǎo)航,選中狀態(tài)底部出現(xiàn)藍(lán)色條塊問題

    react實現(xiàn)頭部導(dǎo)航,選中狀態(tài)底部出現(xiàn)藍(lán)色條塊問題

    這篇文章主要介紹了react實現(xiàn)頭部導(dǎo)航,選中狀態(tài)底部出現(xiàn)藍(lán)色條塊問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2023-11-11
  • antd?table動態(tài)修改表格高度的實現(xiàn)

    antd?table動態(tài)修改表格高度的實現(xiàn)

    本文主要介紹了antd?table動態(tài)修改表格高度的實現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-07-07
  • React 自動聚焦字段使用詳解

    React 自動聚焦字段使用詳解

    這篇文章主要為大家介紹了React 自動聚焦字段使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-07-07

最新評論

房山区| 突泉县| 额敏县| 灯塔市| 宽甸| 田东县| 丰都县| 鹰潭市| 彝良县| 南京市| 勐海县| 同德县| 晋中市| 榆树市| 乐陵市| 宁国市| 洛宁县| 依安县| 大足县| 芒康县| 巴林左旗| 双牌县| 九龙县| 米易县| 攀枝花市| 涞源县| 昌黎县| 酒泉市| 襄垣县| 思茅市| 鹤壁市| 芮城县| 酒泉市| 金堂县| 张北县| 丰都县| 睢宁县| 文山县| 徐汇区| 乐山市| 娄烦县|