十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Next.js App Router 状态共享实战:在 share-state 示例中掌握 Layout 与页面间的 Context 共享

Next.js App Router 状态共享实战:在 share-state 示例中掌握 Layout 与页面间的 Context 共享 Next.js App Router 状态共享实战在 share-state 示例中掌握 Layout 与页面间的 Context 共享【免费下载链接】examplesEnjoy our curated collection of examples and solutions. Use these patterns to build your own robust and scalable applications.项目地址: https://gitcode.com/GitHub_Trending/examples1/examples本篇技术指南以仓库中的 share-state 示例 为蓝本讲解如何在 Next.js App Routerapp 目录下跨 Layout 与各级页面共享 React 状态。读完你将掌握一套可复用的「客户端 Layout 服务端页面 Context Provider」模式以及通过路由组Route Group让根 Layout 保持服务端能力、仅在局部开启客户端共享的进阶方案。示例背景与要解决的问题在 Next.js App Router 中页面Page与布局Layout既可以是服务端组件也可以是客户端组件。当页面需要从服务端拉取数据同时又需要在 Layout 中维护交互状态例如跨嵌套路由共享的计数器、用户偏好、购物车数量时单纯依赖各页面内部useState会带来两个痛点状态只存在于单个页面组件内部无法被 Layout 或兄弟/嵌套路由读取Layout 一旦需要渲染状态就必须成为客户端组件从而失去在服务端渲染子树的能力。share-state示例通过 React Context 将状态提升到 Layout 层并利用「客户端组件不能包含服务端组件但客户端 Layout 可以渲染来自服务端的页面」这一组合实现了状态在整棵路由树中的共享。在线效果可参考示例 app/page.tsx 中给出的 demo 说明。快速开始两种方式运行示例仓库 README 提供了两种使用方式均可直接从本仓库的app-directory/share-state目录出发。方式一一键部署到 Vercel使用 Vercel 的一键部署按钮直接以本示例为模板创建新项目。这种方式无需本地安装依赖适合先快速查看运行效果。方式二克隆后本地运行使用create-next-app配合 pnpm 从本示例目录初始化项目pnpm create next-app --example https://github.com/vercel/examples/tree/main/app-directory/share-state初始化完成后启动开发服务器pnpm dev之后可以执行pnpm build进行生产构建、pnpm start启动生产服务器。以上脚本定义在 package.json 中。需要说明的是该示例声明了engines.node: 22.x并基于next^16、react^19以及 UI 依赖vercel/examples-ui运行前请确认本地 Node 版本匹配。核心模式客户端 Layout 服务端页面示例首页 app/page.tsx 明确阐述了本模式的关键思想尽管客户端组件不能包含服务端组件但我们可以让一个客户端 Layout 去渲染来自服务端的页面。这样我们就能创建一个状态或 Context在整个应用中共享数据。这一约束决定了目录结构的设计负责展示状态的 Layout 和提供状态的 Provider 必须是客户端组件use client而各页面仍保持服务端组件Server Component页面负责渲染可交互的 Counter 客户端组件来变更共享状态。README 中给出的标准文件结构如下/app ├── layout.js (client) # 根布局展示计数器状态 ├── providers.js (client) # 导出 counter context ├── counter.js (client) # 更新计数器的组件 ├── page.js (server) # 渲染 counter 组件 └── nested └── page.js (server) # 嵌套页面同样渲染 counter 组件第一步创建共享 Context 与 Hook状态共享的第一步是创建一个 Context内部存储counter值并导出一个useCounterHook 供组件读取与修改。README 中的简化版本如下// app/providers.js import { createContext, useContext, useState } from react const Counter createContext([0, () {}]) function CounterProvider({ children }) { const state useState(0) return Counter.Provider value{state}{children}/Counter.Provider } const useCounter () useContext(Counter) export { CounterProvider, useCounter }这里的createContext([0, () {}])同时定义了上下文的默认值计数0与空操作函数CounterProvider内部直接使用useState(0)的返回值即[counter, setCounter]元组作为 Context 的 valueuseCounter则是对useContext的一层封装让消费方无需关心 Context 对象本身。在仓库的实际 TypeScript 实现中app/demo/providers.tsx相同的逻辑被补全了完整类型import { createContext, useContext, useState, type ReactNode, type Dispatch, type SetStateAction, } from react type CounterContext [number, DispatchSetStateActionnumber] const Counter createContextCounterContext([0, () {}]) function CounterProvider({ children }: { children: ReactNode }) { const state useState(0) return Counter.Provider value{state}{children}/Counter.Provider } const useCounter () useContext(Counter) export { CounterProvider, useCounter }可以看到CounterContext被显式声明为[number, DispatchSetStateActionnumber]元组类型这正是 ReactuseState返回值的类型保证了 Provider 与消费端在使用上的类型安全。第二步用 Provider 包裹根 Layout 并消费状态接下来把根 Layout 用CounterProvider包裹使useCounter对其下的所有页面可用同时在 Layout 中直接消费counter值进行展示。README 中的简化版本如下// app/layout.js use client import { Link } from next/link import { CounterProvider, useCounter } from ./providers function RootLayout({ children }) { const [counter] useCounter() return ( nav Link href/Index/Link Link href/nestedNested/Link /nav {children} hr / h2Counter/h2 p{counter}/p / ) } export default function RootLayoutContainer(props) { return ( CounterProvider RootLayout {...props} / /CounterProvider ) }这里有一个值得注意的架构细节RootLayout是use client客户端组件但它接收并透传childrenchildren是由服务端渲染的页面子树。这正是「客户端 Layout 渲染服务端页面」的核心——客户端组件不能在自己的模块作用域内导入服务端组件但作为children传入的子树仍可由服务端渲染后注入。仓库的实际实现app/demo/layout.tsx结构与上面一致只是在导航与计数展示上使用了vercel/examples-ui的Page、Link、Text组件并额外提供了一个「← Back to example」返回链接use client import { Link, Page, Text } from vercel/examples-ui import { CounterProvider, useCounter } from ./providers interface Props { children: React.ReactNode } function RootLayout({ children }: Props) { const [counter] useCounter() return ( Page classNameflex flex-col gap-12 nav classNameflex gap-4 Link href/demoIndex/Link Link href/demo/nestedNested/Link /nav {children} section classNameflex flex-col gap-3 Text varianth2Counter/Text Text classNametext-3xl font-black{counter}/Text /section section classNameflex flex-col gap-3 Link href/← Back to example/Link /section /Page ) } export default function RootLayoutContainer(props: Props) { return ( CounterProvider RootLayout {...props} / /CounterProvider ) }注意这里使用了「容器组件」模式默认导出的RootLayoutContainer负责用 Provider 包裹真正渲染布局的RootLayout在 Provider 内部消费useCounter()确保每次渲染都能拿到最新的 Context 值。第三步创建可交互的 Counter 组件Counter 组件负责增减计数值它通过useCounter同时拿到counter和setCounter。README 中的简化版本如下// app/counter.js use client import { useCounter } from ./providers export default function Counter() { const [counter, setCounter] useCounter() return ( div button onClick{() setCounter((counter) counter - 1)} Decrement /button p{counter}/p button onClick{() setCounter((counter) counter 1)} Increment /button /div ) }仓库的实际实现app/demo/counter.tsx使用vercel/examples-ui的Button与Text组件逻辑完全一致。setCounter采用函数式更新(counter) counter - 1/(counter) counter 1这是基于上一次状态的安全写法可避免闭包过期问题。第四步创建页面与嵌套页面最后创建普通页面和嵌套页面两者都渲染同一个Counter组件。由于这两个页面都是服务端组件它们各自负责在服务端渲染 Counter而 Counter 内部的状态读写则经由 Context 指向同一个 Provider——因此无论用户在 Index 页还是 Nested 页点击按钮共享的计数值都会同步反映到 Layout 中。README 中的两个页面实现// app/page.js import Counter from ./counter export default function IndexPage() { return ( div h1Index page/h1 Counter / /div ) }// app/nested/page.js import Counter from ./counter export default function NestedPage() { return ( div h1Nested page/h1 Counter / /div ) }仓库中的对应实现分别为 app/demo/page.tsx 与 app/demo/nested/page.tsx。从源码结构可以看到实际文件树app/demo/ ├── providers.tsx # 导出 CounterProvider 与 useCounter ├── layout.tsx # 客户端布局展示共享计数 ├── counter.tsx # 增减计数组件 ├── page.tsx # Index 页服务端组件 └── nested/ └── page.tsx # Nested 页服务端组件而 app/layout.tsx 是真正的根 Layout服务端组件它通过vercel/examples-ui的Layout组件与getMetadata提供页面元信息并把app/demo子树交给上面这套客户端共享方案处理从而形成了「服务端根布局 客户端局部布局」的分层结构。进阶需要从 Layout 拉取服务端数据时怎么办如果根 Layout 自身需要从服务端获取数据直接把它变成客户端组件会丢失服务端能力。README 给出的解法是创建一个路由组Route Group把客户端共享 Layout 放进组内根 Layout 保持服务端组件不变。调整后的文件结构/app ├── layout.js (server) # 根布局保持服务端能力可拉取数据 └── (app) ├── layout.js (client) # 组内布局客户端承载 Context ├── page.js (server) # 组内页面 ├── providers.js (client) ├── counter.js (client) └── nested └── page.js (server)路由组用括号包裹的目录如(app)不会产生 URL 路径段但可以拥有独立的 Layout。这样根 Layout 继续执行服务端数据获取组内客户端 Layout 负责状态共享组内页面包括嵌套路由则同时受益于两者——既能在服务端获取数据又能在 Layout 与路由之间共享交互状态。运行环境与配置说明示例的 next.config.js 中启用了experimental.appDir/** type {import(next).NextConfig} */ module.exports { experimental: { appDir: true, }, }这是 App Router 早期阶段的显式开关在较新版本的 Next.js 中 App Router 已为默认特性该配置项主要用于兼容旧版本。结合 package.json 中next: ^16.0.10、react: ^19.2.1、react-dom: ^19.2.1的依赖声明与node: 22.x的引擎约束建议在对应的新版本环境下运行本示例以获得最佳兼容性。小结通过share-state示例可以提炼出一套可复用的 App Router 状态共享模式用providers.tsx集中创建 Context 与useXxxHookProvider内部持有useState在客户端 Layout 中用容器组件包裹 Provider并在 Layout 内直接消费状态页面保持服务端组件仅渲染导入的交互组件状态读写全部经由 Context 汇聚到同一 Provider当根 Layout 需要服务端数据时用路由组把「客户端共享 Layout」下沉到组内实现服务端能力与客户端共享的兼得。这一模式不局限于计数器场景任何需要跨 Layout 与嵌套路由共享的客户端状态如主题、购物车、筛选条件都可以按同样的结构落地。【免费下载链接】examplesEnjoy our curated collection of examples and solutions. Use these patterns to build your own robust and scalable applications.项目地址: https://gitcode.com/GitHub_Trending/examples1/examples创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表