前端【免费下载链接】easy-peasyVegetarian friendly state for React项目地址https://gitcode.com/gh_mirrors/ea/easy-peasy点击查看免费下载本篇指南围绕 Easy Peasy 仓库中的examples/nextjs-app-router示例展开讲解 Easy Peasy v7 如何通过新增的easy-peasy/server子路径与 React Server ComponentsRSC及 Next.js App Router 协作服务端组件用瞬态只读 Store 推导 computed 值客户端组件用真正的交互式 Store 完成 UI 状态管理。读完本文你将掌握共享 Model 的组织方式、use client边界的位置、initialState快照传递技巧以及服务端/客户端双 Store 架构下的常见陷阱。一、背景App Router 给状态管理提出的新问题Next.js App Router 默认将组件视为 Server Component它们在服务端执行、不携带 React 状态与交互逻辑。这意味着 Easy Peasy 这一类依赖 React 上下文Context与 hooks 的状态库无法直接在服务端组件中使用 ——StoreProvider、useStoreState等 API 在服务端渲染阶段导入会直接失败。examples/nextjs-app-router示例正是为回答App Router 下 Store 到底该放在哪边而设计。它演示了两件事服务端利用easy-peasy/server子路径构建一个瞬态transientStore纯粹用于在 HTML 输出前推导商品的totalValue计算属性不引入 React、不挂 Provider、也不做 hydration客户端在use client边界内用useRef创建真正的交互式 Store并通过StoreProvider包裹子树服务端取回的数据以initialState传入作为初始状态。该示例对应的正是官方 RSC / App Router 配方 中的Pattern 2服务端准备快照 客户端 Store与Pattern 3服务端组件内的只读 Store的组合。二、示例项目结构一览示例是一个独立的 Next.js 应用位于examples/nextjs-app-router核心文件如下文件角色app/layout.tsx根布局Server Component仅渲染childrenapp/page.tsx首页 Server Component取数 服务端推导总额app/products-client.tsxuse client边界创建交互式 Store 并挂 Providerapp/products-view.tsx客户端组件通过类型化 hooks 消费 Storelib/model.ts共享 Model服务端与客户端共同导入lib/hooks.ts类型化 hookscreateTypedHookslib/data.ts模拟异步取数种子商品数据依赖环境见 examples/nextjs-app-router/package.jsoneasy-peasy^7.0.0-beta.2、next^15.5.15、react/react-dom19.0.0并提供dev/build/start三个脚本。此外 tsconfig.json 配置了/*指向项目根的路径别名示例中/lib/model即映射到lib/model。三、核心基础easy-peasy/serverReact-free 子路径3.1 为什么需要独立子路径easy-peasy/server是 v7 新增的无 React 子路径导出专用于导入 React 反而是错误的环境React Server Components、Edge Runtime、以及需要在无组件树条件下构建 Store 快照的 Node.js 脚本。它的设计目标是完整导出 Store 与 Model API但完全不导出任何 React hooks 与StoreProvider——这样你可以在服务端构建 Store、dispatch action、读取 state、序列化结果而不会把 React 打包进服务端 bundle。完整的导出清单与说明见 easy-peasy-server API 文档要点如下导出的 Store 工厂createStore、createTransform导出的辅助函数action、actionOn、computed、debug、effectOn、generic、persist、reducer、thunk、thunkOn导出的类型Store、EasyPeasyConfig、Action、ActionOn、Thunk、ThunkOn、Computed、EffectOn、Reducer、Dispose、Listeners、Actions、State、Dispatch、ReduxAction、MockedAction、AddModelResult、PersistStorage、PersistConfig、TransformConfig、Transformer、Generic刻意不导出StoreProvider、全部use*hooksuseStoreState、useStoreActions、useStoreDispatch、useStore、useStoreRehydrated、useStoreTransition、useStoreDeferredState、useStoreOptimistic、createContextStore、useLocalStore、createTypedHooks。这些 React 绑定 API 只能在标注了use client的组件里从主入口easy-peasy导入。3.2 源码级验证从仓库源码可以确认这一设计确实落实到了打包与类型层面src/server.js 是子路径的实现入口仅export了createStore、createTransform与 helpers没有引入任何 hooks/Providerserver.d.ts 为子路径提供完整类型声明把上表中的类型从主入口./index重新导出rollup.config.js 显示构建时主入口dist/index.js会注入use client;banner而服务端入口dist/server.js的 banner 为空——这正好与服务端组件不得标记 use client的要求吻合package.json 的exports字段声明了./server子路径及其类型入口./server.d.tstests/server-export.test.js 从测试层面锁定契约server入口必须暴露createStore/createTransform/各 helper且server.useStoreState、server.StoreProvider、server.createTypedHooks等必须为undefined同时验证了从easy-peasy/server创建的 Store 可以正常getState()与 dispatch action。四、共享 Model一份模型两端使用示例最关键的设计决策是把 Model 放在 React 树之外lib/model.ts不依赖任何 React 代码因此既可以被服务端组件导入也可以被客户端组件导入甚至可以从easy-peasy/server与主入口分别导入Model 本体只是纯数据 辅助函数。lib/model.ts 完整内容如下import { Action, Computed, action, computed, } from easy-peasy/server; export type Product { id: string; name: string; category: string; price: number; }; export interface ProductsModel { items: Product[]; query: string; setQuery: ActionProductsModel, string; addItem: ActionProductsModel, Product; totalValue: ComputedProductsModel, number; filtered: ComputedProductsModel, Product[]; } export const productsModel: ProductsModel { items: [], query: , setQuery: action((state, payload) { state.query payload; }), addItem: action((state, payload) { state.items.push(payload); }), totalValue: computed((state) state.items.reduce((sum, item) sum item.price, 0), ), filtered: computed((state) { const q state.query.toLowerCase(); if (!q) return state.items; return state.items.filter((item) item.name.toLowerCase().includes(q), ); }), };两个值得注意的细节action使用 Immer 语义setQuery直接对state.query赋值、addItem直接state.items.push无需手动返回新对象。Easy Peasy v7 内部默认集成 ImmercreateStore的disableImmer选项默认为false参见 src/create-store.jscomputed承担派生逻辑totalValue汇总所有商品价格filtered根据query过滤商品名。服务端复用模型逻辑计算属性、数据归一化而无需任何交互性正是 Pattern 3 的典型场景。模拟数据 lib/data.ts 提供了 6 条商品Bananas、Sourdough Loaf、Almond Milk、Carrots、Olive Oil、Apples并通过fetchProducts()模拟 50ms 的异步取数。五、Pattern 3服务端组件内的瞬态只读 Storeapp/page.tsx 是一个 async Server Component完整代码如下import { createStore } from easy-peasy/server; import { fetchProducts } from /lib/data; import { productsModel } from /lib/model; import { ProductsClient } from ./products-client; export default async function HomePage() { const products await fetchProducts(); const serverStore createStore(productsModel, { initialState: { items: products, query: }, }); const { totalValue } serverStore.getState(); return ( main h1Easy Peasy v7 — App Router RSC/h1 p This server component fetches products, builds a transient store via code easy-peasy/server/code to derive a total via the modelapos;s computed property, then hands the data to a client component which owns the interactive store. /p p strongServer-rendered total:/strong{ } ${totalValue.toFixed(2)} /p ProductsClient initialItems{products} / /main ); }这段代码清晰展示了 Pattern 3 的核心动作从easy-peasy/server导入createStore服务端 bundle 不会拖入 Reactawait fetchProducts()获取数据用createStore(productsModel, { initialState: { items: products, query: } })构建一个瞬态 Store——它只服务于这一次请求用于触发totalValue计算属性的求值通过serverStore.getState()读取推导结果直接渲染成 HTML 里的Server-rendered total: $X.XX把原始商品数组以initialItemsprop 交给客户端组件。服务端这里不会共享这个 Store 给客户端客户端会构建自己的 Store见下节。createStore的initialState选项在 src/create-store.js 被解构取出并在bindStoreInternals(initialState || {})src/create-store.js中作为模型初始数据灌入 Redux Store——这就是initialState能够直接预填整个 state 树的底层机制。六、Pattern 2客户端真正的交互式 Store6.1use client边界与 useRef 单例app/products-client.tsx 是服务端与客户端的分界线完整代码如下use client; import { useRef } from react; import { Store, StoreProvider, createStore } from easy-peasy; import { productsModel } from /lib/model; import type { Product, ProductsModel } from /lib/model; import { ProductsView } from ./products-view; export function ProductsClient({ initialItems }: { initialItems: Product[] }) { const storeRef useRefStoreProductsModel | null(null); if (storeRef.current null) { storeRef.current createStore(productsModel, { initialState: { items: initialItems, query: }, }); } return ( StoreProvider store{storeRef.current} ProductsView / /StoreProvider ); }要点解析use client指令这里从主入口easy-peasy导入createStore与StoreProvider因此必须是客户端组件useRef惰性单例if (storeRef.current null)只在首次渲染时执行createStore保证每个 Provider 实例只创建一个 Store。绝不能在模块作用域module scope创建 Store——在 RSC/App Router 下同一模块实例会被跨请求、跨用户复用一个用户的 action 会污染另一个用户看到的 state。Store 要么绑定在客户端组件实例的生命周期内本示例要么绑定在服务端组件函数体的一次请求内上一节快照即初值initialState: { items: initialItems, query: }把服务端取好的商品直接灌入客户端 Store避免重复取数和空 UI 闪烁。6.2 类型化 hooks 消费 Storelib/hooks.ts 通过createTypedHooksProductsModel()生成与模型类型绑定的 hooksimport { createTypedHooks } from easy-peasy; import type { ProductsModel } from ./model; export const { useStoreActions, useStoreState, } createTypedHooksProductsModel();之后 app/products-view.tsx 便能用带完整类型推断的 hooks 读取状态、派发 actionuse client; import { ChangeEvent } from react; import { useStoreActions, useStoreState } from /lib/hooks; export function ProductsView() { const query useStoreState((state) state.query); const filtered useStoreState((state) state.filtered); const total useStoreState((state) state.totalValue); const setQuery useStoreActions((actions) actions.setQuery); function handleQueryChange(event: ChangeEventHTMLInputElement) { setQuery(event.target.value); } return ( section p strongClient total:/strong ${total.toFixed(2)} /p p input onChange{handleQueryChange} placeholderFilter by name… typetext value{query} / /p ul {filtered.map((product) ( li key{product.id} {product.name} — {product.category} — ${product.price.toFixed(2)} /li ))} /ul /section ); }输入框的值绑定queryonChange触发setQueryactionfiltered计算属性随之重算列表实时响应——这正是 Pattern 2 交互式 Store 的工作方式。在底层useStoreState基于 React 的useSyncExternalStore实现订阅与选择器参见 src/hooks.js并通过useDebugValue暴露调试信息。七、运行示例在examples/nextjs-app-router目录下执行yarn install yarn dev然后打开 http://localhost:3000。页面上会同时出现两处合计金额Server-rendered total由服务端组件通过easy-peasy/server构建的瞬态 Store 推导直接渲染进 HTMLClient total由客户端交互式 Store 中的同一totalValuecomputed 属性得出。两者数值一致种子数据合计为 29.25但来自两条完全独立的 Store 实例。你可以在输入框中输入 a 之类的字符观察filtered列表实时过滤。八、集成要点与常见陷阱8.1 禁止在模块作用域共享 Store// ❌ 错误每个请求都会复用这一个实例 import { createStore } from easy-peasy/server; import { model } from ./model; export const sharedStore createStore(model);在传统 Node 风格 SPA 中模块作用域 Store 没问题但在 RSC/App Router 应用中同一模块实例会被跨请求、跨并发用户复用。务必在客户端组件实例内Pattern 1/2或服务端组件函数体内Pattern 3创建 Store。8.2 hooks 只能存在于客户端子树StoreProvider、useStoreState、useStoreActions、useStoreTransition、useStoreRehydrated等都需要 React 与 Provider从服务端组件导入会在构建/渲染期失败。服务端需要模型逻辑时一律从easy-peasy/server导入。8.3 服务端 → 客户端的 props 必须可 JSON 序列化从服务端组件传给客户端组件的任何 prop 都会穿越服务端/客户端边界被序列化。Date、Map、Set、BigInt、函数、类实例以及undefined要么抛错、要么往返后值不正确。模型持有非序列化值时应在边界转换——例如把Date序列化为 ISO 字符串后在客户端再恢复或利用createTransform管道统一处理persist 场景同理// 服务端组件内 return ProductsClient initialItems{products.map((p) ({ ...p, createdAt: p.createdAt.toISOString(), // 规范化为字符串 }))} /;8.4 服务端不要依赖 persist 的浏览器存储默认的sessionStorage/localStorage存储引擎在服务端并不存在。如果 Model 使用了persist又在easy-peasy/server上构建瞬态 Store需要为服务端场景配置 no-op 或内存存储引擎或干脆在该代码路径跳过持久化否则 rehydration 会抛错或挂起。8.5useStoreRehydrated与流式渲染App Router 支持通过Suspense边界与loading.tsx流式渲染useStoreRehydrated会在持久化状态 rehydrate 期间挂起天然与之配合rehydration 只发生在客户端所以服务端渲染的 HTML 会先输出 Suspense fallback客户端 Store 读取浏览器存储后 fallback 被替换。如果希望服务端直接渲染真实内容而非 fallback如 SEO/预览场景应改用 Pattern 2——由服务端通过initialState快照喂数据Store 首次渲染即已就绪useStoreRehydrated在第一帧便解析完成。九、总结examples/nextjs-app-router展示的服务端瞬态 Store 推导 客户端交互 Store 接管组合是 Easy Peasy v7 在 RSC/App Router 时代的推荐集成方式一份共享 Modellib/model.ts服务两端computed 逻辑totalValue、filtered在服务端与客户端复用同一份实现easy-peasy/server保证服务端 bundle 零 React 依赖构建瞬态 Store 只为推导不挂 Provider、不 hydrationuse client边界 useRefinitialState让客户端 Store 既能接收服务端快照又能提供完整的交互体验底层契约由 src/server.js、server.d.ts 与 tests/server-export.test.js 共同锁定可放心在项目中使用。若需了解更多端到端模式use client放置位置、避免模块作用域 Store 陷阱、快照后水合等可继续阅读 RSC / App Router 配方 与 easy-peasy/server API 文档。赞分享前端【免费下载链接】easy-peasyVegetarian friendly state for React项目地址https://gitcode.com/gh_mirrors/ea/easy-peasy点击查看免费下载相关推荐10分钟学会使用PotatoNV华为Kirin设备Bootloader解锁快速入门10分钟学会使用PotatoNV华为Kirin设备Bootloader解锁快速入门 PotatoNV是一款专门为华为Kirin系列芯片设备设计的Bootloa开发工具嵌入式固件Convex Next.js App Router SSR 实战在 Server Components 中预加载数据Convex Next.js App Router SSR 实战在 Server Components 中预加载数据 导读 本文基于开源仓库 convex数据库后端Lingui 在 Next.js App Router 中实现 React Server Components 国际化RSC i18n 完整实战指南Lingui 在 Next.js App Router 中实现 React Server Components 国际化RSC i18n 完整实战指南 Ling开发工具前端创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考