如何在tRPC中拦截请求,返回localStorage数据替代数据库实时数据?
实现本地/实时数据切换的几种方案
拦截tRPC请求的最优方案
tRPC的**链接(Link)**机制天生支持拦截请求/响应,这比在每个useQuery里写重复逻辑靠谱多了,能统一处理所有请求的切换逻辑。
1. 写一个本地数据拦截链接
这个链接会在请求发出去前判断是否启用本地模式,如果是,直接返回localStorage里的数据,跳过真实API请求:
import { TRPCLink } from '@trpc/client'; export const localStorageLink: TRPCLink<any> = () => { return ({ next, op }) => { // 从状态管理里拿当前是否启用本地模式(比如用Context或者Zustand) const useLocalData = getLocalDataMode(); // 自己实现状态获取逻辑 if (useLocalData) { // 根据接口路径取对应本地数据 const localData = localStorage.getItem(op.path); if (localData) { return Promise.resolve({ result: { type: 'data', data: JSON.parse(localData), }, }); } // 本地没数据的话,还是走真实请求兜底 } // 不启用本地模式就正常发请求 return next(op); }; };
2. 把链接加到tRPC客户端里
初始化tRPC客户端时,把这个拦截链接放在最前面,确保优先触发:
import { createTRPCNext } from '@trpc/next'; import { localStorageLink } from './localStorageLink'; export const api = createTRPCNext<AppRouter>({ config() { return { links: [ localStorageLink, // 先拦截,再走http链接 httpBatchLink({ url: '/api/trpc', }), ], }; }, // 其他配置按需添加 });
3. 做个切换状态的管理
用React Context或者状态管理库来存切换按钮的状态,让拦截链接能拿到这个状态。比如用Context:
// context/LocalDataContext.tsx 'use client'; import { createContext, useContext, useState, ReactNode } from 'react'; interface LocalDataContextType { useLocalData: boolean; toggleLocalData: () => void; } const LocalDataContext = createContext<LocalDataContextType | undefined>(undefined); export function LocalDataProvider({ children }: { children: ReactNode }) { const [useLocalData, setUseLocalData] = useState(false); const toggleLocalData = () => { setUseLocalData(prev => !prev); }; return ( <LocalDataContext.Provider value={{ useLocalData, toggleLocalData }}> {children} </LocalDataContext.Provider> ); } export function useLocalData() { const context = useContext(LocalDataContext); if (!context) { throw new Error('useLocalData必须在LocalDataProvider内使用'); } return context; }
要是用Zustand的话,直接在链接里调用store.getState().useLocalData更方便,不用纠结Hook的使用场景。
替代方案:封装自定义Hook
不想改tRPC链接的话,也可以写个自定义Hook把useQuery包起来,统一处理数据切换:
import { useLocalData } from './context/LocalDataContext'; export function useLocalOrRemoteQuery<T>( queryFn: () => { data: T | undefined; refetch: () => Promise<void> }, localStorageKey: string ) { const { useLocalData } = useLocalData(); const trpcResult = queryFn(); if (useLocalData) { const localData = localStorage.getItem(localStorageKey); return { ...trpcResult, data: localData ? JSON.parse(localData) : trpcResult.data, }; } return trpcResult; }
用的时候直接替换原来的useQuery:
const { data } = useLocalOrRemoteQuery( () => api.product.list.useQuery(), 'product-list' );
对你原来代码的问题说明
你之前写的onSuccess逻辑是无效的:一是onSuccess是请求成功后才触发,这时候真实API请求已经发出去了,达不到拦截的目的;二是onSuccess的返回值不会修改useQuery的data,所以本地数据不会被用到。
额外提醒
- 一定要保证localStorage里的数据格式和tRPC返回的完全一致,不然前端渲染会报错;
- 可以在切换到本地模式时,先把实时数据缓存到localStorage,避免本地没数据的尴尬;
- 对于修改类的Mutation接口,本地模式下最好直接禁用,或者提示用户切回实时模式再操作,毕竟本地改了也同步不到数据库。
内容的提问来源于stack exchange,提问作者Meriegg
相关产品推荐
相关产品推荐

