Next.js 14中使用Zustand实现布局数据共享至客户端组件的问题
Next.js 14 服务器组件数据传递到 Zustand 客户端状态解决方案
核心问题分析
Zustand 是客户端状态管理库,默认无法直接读取服务器组件的数据。你遇到的问题根源在于:服务器组件获取的数据没有正确初始化客户端的 Zustand store,导致客户端组件拿到空数组;同时服务器组件重复请求数据可以通过数据透传来避免。
步骤1:创建支持初始值的 Zustand Store
需要封装一个可接收初始数据的 store 工厂函数,并配合 React Context 让客户端组件能访问到 store 实例(因为服务器组件不能直接初始化 Zustand store)。
// app/store/products.ts import { create } from 'zustand'; import { createContext, useContext } from 'react'; // 定义产品类型 type Product = { id: string; name: string; price: number }; // Store 状态类型 type ProductStore = { products: Product[]; setProducts: (products: Product[]) => void; }; // 创建 Store 的工厂函数,接收初始数据 const createProductStore = (initialProducts: Product[]) => { return create<ProductStore>((set) => ({ products: initialProducts, setProducts: (newProducts) => set({ products: newProducts }), })); }; // 创建 Context 用于传递 Store 实例 const ProductStoreContext = createContext<ReturnType<typeof createProductStore> | null>(null); // 客户端 Provider 组件(必须标记为 use client) 'use client'; export const ProductStoreProvider = ({ children, initialProducts }: { children: React.ReactNode; initialProducts: Product[] }) => { const store = createProductStore(initialProducts); return ( <ProductStoreContext.Provider value={store}> {children} </ProductStoreContext.Provider> ); }; // 客户端组件使用的 Hook export const useProductStore = () => { const store = useContext(ProductStoreContext); if (!store) throw new Error('useProductStore 必须在 ProductStoreProvider 内部使用'); return store; };
步骤2:在服务器组件 Layout 中获取数据并注入 Provider
在根布局(服务器组件)中获取数据,然后用客户端的 ProductStoreProvider 包裹整个应用,将初始数据传递进去。
// app/layout.tsx import { ProductStoreProvider } from './store/products'; // 服务器端数据请求函数 async function fetchProducts() { const res = await fetch('https://api.example.com/products', { cache: 'force-cache' // 根据业务需求调整缓存策略,比如 'no-store' 禁用缓存 }); if (!res.ok) throw new Error('获取产品数据失败'); return res.json(); } export default async function RootLayout({ children }: { children: React.ReactNode }) { // 服务器端获取初始数据 const initialProducts = await fetchProducts(); return ( <html lang="zh-CN"> <body> {/* 将初始数据传递给客户端 Provider */} <ProductStoreProvider initialProducts={initialProducts}> {children} </ProductStoreProvider> </body> </html> ); }
步骤3:客户端组件读取 Zustand Store
在客户端组件中直接使用 useProductStore 获取数据,此时初始数据已经从服务器传递过来,不会是空数组。
// app/components/ClientOverview.tsx 'use client'; import { useProductStore } from '../store/products'; export default function ClientOverview() { const products = useProductStore((state) => state.products); return ( <div className="product-list"> <h3>产品概览</h3> {products.length === 0 ? ( <p>暂无产品数据</p> ) : ( products.map((product) => ( <div key={product.id} className="product-item"> {product.name} - ¥{product.price} </div> )) )} </div> ); }
步骤4:避免服务器组件重复请求数据
如果你的 page.tsx 也是服务器组件,不需要重复请求数据,可以通过布局透传 props 的方式将数据传递给页面:
修改 layout.tsx 中的 children 部分:
// app/layout.tsx // ... return ( <html lang="zh-CN"> <body> <ProductStoreProvider initialProducts={initialProducts}> {/* 将初始数据透传给页面组件 */} {React.cloneElement(children as React.ReactElement, { initialProducts })} </ProductStoreProvider> </body> </html> );
然后在 page.tsx 中直接使用透传的数据:
// app/page.tsx import ClientOverview from './components/ClientOverview'; type Product = { id: string; name: string; price: number }; export default function HomePage({ initialProducts }: { initialProducts: Product[] }) { // 直接使用布局传递的数据,无需重复请求 return ( <main> <h1>首页</h1> <ClientOverview /> </main> ); }
关键注意事项
- Zustand Store 必须在客户端初始化:服务器组件不能直接创建 Zustand store,必须通过标记
use client的 Provider 组件来初始化。 - 初始数据从服务器透传:通过服务器组件布局获取数据,传递给客户端 Provider,确保客户端 store 一开始就有初始值。
- 服务器请求缓存:Next.js 14 中服务器组件的
fetch默认会缓存请求结果,即使多个组件调用同一个请求函数,也只会发起一次请求;透传 props 则进一步避免了不必要的函数调用。
内容的提问来源于stack exchange,提问作者StackMyFlow
相关产品推荐
相关产品推荐

