You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

NextJS中使用Zustand实现服务端数据获取及缓存的正确方案

解决NextJS + Zustand 首次加载数据可用及缓存的正确实现

方案1:服务器端预加载 + 客户端Store初始化

如果你的场景需要首屏直接拿到数据,这个方案最适合——利用NextJS服务器组件的能力提前拉取数据,直接注入Zustand Store作为初始值,彻底避免客户端useEffect带来的延迟。

步骤:

  1. 改造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),
          }
        )
      );
    };
    
  2. 在服务器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>
      );
    }
    
  3. 实现客户端的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;
    };
    
  4. 组件中直接使用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。

步骤:

  1. 改造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),
        }
      )
    );
    
  2. 在客户端根组件初始化数据
    在客户端根组件中调用异步方法,确保首次加载时触发数据请求:

    // 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>
      );
    }
    
  3. 组件中直接使用数据
    组件无需处理请求逻辑,直接从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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 08:38:14