NextJS中使用Zustand实现服务端数据获取及缓存的正确方案
解决NextJS + Zustand 首次加载数据可用及缓存的正确实现
方案1:服务器端预加载 + 客户端Store初始化
如果你的场景需要首屏直接拿到数据,这个方案最适合——利用NextJS服务器组件的能力提前拉取数据,直接注入Zustand Store作为初始值,彻底避免客户端useEffect带来的延迟。
步骤:
改造Zustand Store,支持外部传入初始值
让Store可以接收外部初始数据,同时保留持久化配置:import { create } from 'zustand'; import { persist, createJSONStorage } from 'zustand/middleware'; // 定义数据类型 type UserData = { id: string; name: string; email: string; }; // 定义Store类型 type AppStore = { userData: UserData | null; setUserData: (data: UserData | null) => void; clearUserData: () => void; }; // 创建可接收初始值的Store工厂函数 export const createAppStore = (initialData?: UserData) => { return create<AppStore>()( persist( (set) => ({ // 优先使用传入的初始值,无初始值则用null(持久化数据会覆盖此默认值) userData: initialData || null, setUserData: (data) => set({ userData: data }), clearUserData: () => set({ userData: null }), }), { name: 'app-storage', // 持久化存储的key storage: createJSONStorage(() => localStorage), } ) ); };在服务器Layout中获取数据,传给客户端组件初始化Store
利用NextJS服务器组件的能力直接请求数据,再通过客户端组件传递给Store:// app/layout.tsx(服务器组件) import { fetchUserData } from '@/lib/api'; import StoreProvider from './store-provider'; export default async function RootLayout({ children }: { children: React.ReactNode }) { // 服务器端预加载数据,可通过NextJS的fetch缓存策略优化 const userData = await fetchUserData(); return ( <html lang="zh-CN"> <body> {/* 将服务器获取的数据传给客户端StoreProvider */} <StoreProvider initialData={userData}> {children} </StoreProvider> </body> </html> ); }实现客户端的StoreProvider
这个客户端组件负责初始化Store并提供给所有子组件:// app/store-provider.tsx(客户端组件,必须加'use client'指令) 'use client'; import { createAppStore } from '@/store/app-store'; import { ReactNode, createContext, useContext, useRef } from 'react'; const StoreContext = createContext<ReturnType<typeof createAppStore> | null>(null); export default function StoreProvider({ children, initialData }: { children: ReactNode; initialData: UserData }) { // 用useRef避免重复创建Store const storeRef = useRef<ReturnType<typeof createAppStore>>(); if (!storeRef.current) { storeRef.current = createAppStore(initialData); } return ( <StoreContext.Provider value={storeRef.current}> {children} </StoreContext.Provider> ); } // 自定义Hook,方便组件快速获取Store export const useAppStore = () => { const store = useContext(StoreContext); if (!store) throw new Error('useAppStore必须在StoreProvider内部使用'); return store; };组件中直接使用Store数据
任何客户端组件都可以直接调用useAppStore拿到数据,无需等待useEffect执行:// app/page.tsx(客户端组件) 'use client'; import { useAppStore } from './store-provider'; export default function HomePage() { const userData = useAppStore((state) => state.userData); if (!userData) { return <div>加载中...</div>; // 仅在持久化数据不存在且服务器预加载失败时显示 } return <div>欢迎回来,{userData.name}!</div>; }
方案2:Zustand内置异步初始化 + 本地缓存
如果你的场景不需要服务器预加载,希望客户端自主处理数据请求,这个方案更合适——让Store自己负责数据请求、缓存判断和持久化,无需依赖外部Context和useEffect。
步骤:
改造Store,加入异步获取和缓存逻辑
在Store中添加带缓存判断的异步方法,仅当数据过期或不存在时才发起请求:import { create } from 'zustand'; import { persist, createJSONStorage } from 'zustand/middleware'; type UserData = { id: string; name: string; email: string; cacheExpiry: number; // 缓存过期时间戳 }; type AppStore = { userData: UserData | null; fetchAndSetUserData: () => Promise<void>; clearUserData: () => void; }; // 缓存有效时长:1小时(单位:毫秒) const CACHE_TTL = 3600 * 1000; export const useAppStore = create<AppStore>()( persist( (set, get) => ({ userData: null, fetchAndSetUserData: async () => { const currentData = get().userData; // 判断缓存是否有效:存在且未过期 if (currentData && Date.now() < currentData.cacheExpiry) { return; // 直接使用缓存,不发起请求 } // 缓存无效时请求新数据 const res = await fetch('/api/user'); const rawData = await res.json(); // 为数据添加缓存过期时间 const dataWithCache = { ...rawData, cacheExpiry: Date.now() + CACHE_TTL }; set({ userData: dataWithCache }); }, clearUserData: () => set({ userData: null }), }), { name: 'app-storage', storage: createJSONStorage(() => localStorage), } ) );在客户端根组件初始化数据
在客户端根组件中调用异步方法,确保首次加载时触发数据请求:// app/client-initializer.tsx(客户端组件) 'use client'; import { useAppStore } from '@/store/app-store'; import { useEffect } from 'react'; export default function ClientInitializer() { const fetchAndSetUserData = useAppStore((state) => state.fetchAndSetUserData); useEffect(() => { fetchAndSetUserData(); }, [fetchAndSetUserData]); return null; // 仅做初始化,不渲染内容 }在服务器Layout中引入这个初始化组件:
// app/layout.tsx(服务器组件) import { ReactNode } from 'react'; import ClientInitializer from './client-initializer'; export default async function RootLayout({ children }: { children: ReactNode }) { return ( <html lang="zh-CN"> <body> <ClientInitializer /> {children} </body> </html> ); }组件中直接使用数据
组件无需处理请求逻辑,直接从Store获取数据即可:// app/page.tsx(客户端组件) 'use client'; import { useAppStore } from '@/store/app-store'; export default function HomePage() { const userData = useAppStore((state) => state.userData); if (!userData) { return <div>加载中...</div>; } return <div>欢迎回来,{userData.name}!</div>; }
关键注意事项
- 服务器端缓存优化:在服务器端调用
fetch时,可通过next.revalidate配置设置缓存有效期,比如fetch('/api/user', { next: { revalidate: 3600 } }),避免重复请求。 - 持久化与缓存协同:如果开启了持久化,一定要加上缓存过期判断,防止本地存储的旧数据一直被使用。
- 避免重复创建Store:客户端组件中务必用
useRef或者单例模式创建Store,防止每次渲染都重新初始化,导致数据丢失。
内容的提问来源于stack exchange,提问作者Salman Fazal
相关产品推荐
相关产品推荐

