Laucher
2024年 04月 15日
Next.js中的基本状态管理
这篇Blog提供了关于在 Next.js 中进行基本状态管理的综合指南。它涵盖了使用 useState 和 useReducer 进行本地状态管理,以及使用 Context API 进行全局状态管理的方法。此外,它还介绍了如何从外部 API 获取数据,并使用 SWR 简化数据获取和状态管理的过程。最后还探讨了 Next.js 中的中间件集成以及服务器组件中的状态管理技术。
Next.js 中的基本状态管理
让我们从 Next.js 状态管理的基础开始。在接下来的章节中,我们将讨论基本的 Hooks、技术和工具,以便有效地管理状态。
使用 useState Hook 进行状态管理
useState Hook 是在传统的 React 应用中管理状态的一种常用方法,在 Next.js 中也可以类似地使用。让我们创建一个允许用户通过点击按钮增加分数的应用。
进入 pages 文件夹,在 index.js 中包含以下代码:
// 'use client' // 如果使用 /app 文件夹
import { useState } from "react";
export default function Home() {
const [score, setScore] = useState(0);
const increaseScore = () => setScore(score + 1);
return (
<div>
<p>你的分数是 {score}</p>
<button onClick={increaseScore}>+</button>
</div>
);
}
我们首先导入了 useState Hook,然后将初始状态设置为 0。我们还提供了一个 setScore 函数,以便稍后更新分数。
然后我们创建了 increaseScore 函数,它访问分数的当前值,并使用 setState 将其增加 1。我们将该函数分配给了 + 按钮的 onClick 事件,因此每次按下按钮时,分数都会增加。
useReducer Hook
useReducer Hook 的工作方式类似于数组的 reduce 方法。我们传递一个 reducer 函数和一个初始值。reducer 接收当前状态和一个操作,并返回新的状态。
我们将创建一个应用,允许您将当前活动结果乘以 2。在 index.js 中包含以下代码:
// 'use client' // 如果使用 /app 文件夹
import { useReducer } from "react";
export default function Home() {
const [multiplication, dispatch] = useReducer((state, action) => {
return state * action;
}, 50);
return (
<div>
<p>{multiplication}</p>
<button onClick={() => dispatch(2)}>X2</button>
</div>
);
}
首先,我们导入了 useReducer Hook 本身。我们传递了 reducer 函数和初始状态。Hook 然后返回当前状态和 dispatch 函数的数组。
我们将 dispatch 函数传递给了 onClick 事件,以便每次单击按钮时,当前状态值都会乘以 2,分别设置为以下值:100、200、400、800、1600,依此类推。
用于 Next.js 中状态管理的 prop drilling 技术
在更复杂的应用程序中,您不会直接在单个文件中处理状态。您很可能会将代码分成不同的组件,以便更容易扩展和维护应用程序。
一旦有多个组件,状态就需要从父级传递到子级。这种技术称为 prop drilling,可以是多层深度的。
对于本教程,我们将创建一个基本示例,只深入了两个级别,以便让您了解 prop drilling 的工作原理。将以下代码包含到 index.js 文件中:
// 'use client' // 如果使用 /app 文件夹
import { useState } from "react";
const Message = ({ active }) => {
return <h1>The switch is {active ? "active" : "disabled"}</h1>;
};
const Button = ({ onToggle }) => {
return <button onClick={onToggle}>Change</button>;
};
const Switch = ({ active, onToggle }) => {
return (
<div>
<Message active={active} />
<Button onToggle={onToggle} />
</div>
);
};
export default function Home() {
const [active, setActive] = useState(false);
const toggle = () => setActive((active) => !active);
return <Switch active={active} onToggle={toggle} />;
}
在上面的代码片段中,Switch 组件本身不需要 active 和 toggle 值,但我们必须“穿透”组件,并将这些值传递给需要它们的子组件 Message 和 Button。
在 Next.js 中使用 Context API
useState 和 useReducer Hooks 与 prop drilling 技术相结合,可以涵盖大多数您构建的基本应用程序的用例。
但是,如果您的应用程序更复杂,需要将 props 传递到多个级别,或者有一些需要全局访问的状态,该怎么办呢?
在这种情况下,建议避免 prop drilling,并使用 Context API,它将允许您全局访问状态。
在不同的状态(如身份验证、用户数据等)上创建单独的上下文始终是一个很好的实践。我们将为主题状态管理创建一个示例。
首先,在根目录中创建一个名为 context 的单独文件夹。在其中创建一个名为 theme.js 的新文件,并包含以下代码:
import { createContext, useContext, useState } from "react";
const Context = createContext();
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState("light");
return (
<Context.Provider value={[theme, setTheme]}>{children}</Context.Provider>
);
}
export function useThemeContext() {
return useContext(Context);
}
我们首先创建了一个新的 Context 对象,创建了一个 ThemeProvider 函数,并将 Context 的初始值设置为 light。
然后,我们创建了一个自定义 useThemeContext Hook,它将允许我们在应用程序的各个页面或组件中访问主题状态。
接下来,我们需要将 ThemeProvider 包装在整个应用程序周围,以便我们可以在整个应用程序中访问主题状态。转到 _app.js 文件,并包含以下代码:
import { ThemeProvider } from "../context/theme";
export default function MyApp({ Component, pageProps }) {
return (
<ThemeProvider>
<Component {...pageProps} />
</ThemeProvider>
);
}
要访问主题的状态,请转到 index.js,并包含以下代码:
// 'use client' // 如果使用 /app 文件夹
import Link from "next/link";
import { useThemeContext } from "../context/theme";
export default function Home() {
const [theme, setTheme] = useThemeContext();
return (
<div>
<h1>Welcome to the Home page</h1>
<Link href="/about">
<a>关于</a>
</Link>
<p>当前主题: {theme}</p>
<button
onClick={() => {
theme == "light" ? setTheme("dark") : setTheme("light");
}}
>
切换主题
</button>
</div>
);
}
我们首先导入了 useThemeContext,然后访问了主题状态和 setTheme 函数,在需要时进行更新。
在切换按钮的 onClick 事件中,我们创建了一个更新函数,它在 light 和 dark 之间切换相反的值,具体取决于当前值。
通过路由访问 Context
Next.js 使用一个 pages 文件夹来创建应用程序中的新路由。例如,如果您创建了一个名为 route.js 的新文件,然后从某处通过 Link 组件引用它,它将通过 URL 在您的应用程序中访问 /route。
在前面的代码片段中,我们创建了一个到 About 路由的路由。这将允许我们测试主题状态是否在全局可访问。
此路由当前不存在,因此让我们在 pages 文件夹中创建一个名为 about.js 的新文件,并包含以下代码:
// 'use client' // 如果使用 /app 文件夹
import Link from "next/link";
import { useThemeContext } from "../context/theme";
export default function Home() {
const [theme, setTheme] = useThemeContext();
return (
<div>
<h1>About页面,HELLO WORLD</h1>
<Link href="/">
<a>home</a>
</Link>
<p>当前活动主题: {theme}</p>
<button
onClick={() => {
theme == "light" ? setTheme("dark") : setTheme("light");
}}
>
切换主题
</button>
</div>
);
}
我们创建了一个非常类似的代码结构,我们在前面的 Home 路由中使用了。唯一的区别是页面标题和不同的链接,用于返回到 Home。
现在,尝试切换当前活动的主题,并在不同的路由之间切换。请注意,状态在两个路由中都得到了保留。您还可以创建不同的组件,而主题状态将在应用程序文件树的任何位置访问到。
从 API 中获取数据
前面的方法适用于在应用程序内部管理状态的情况。但在实际情况下,您很可能会通过 API 从外部获取一些数据。
数据获取可以概括为向 API 端点发出请求,并在请求处理完成并发送响应后接收数据。
我们需要考虑到这个过程不是立即的,因此我们需要管理响应状态,例如等待时间的状态,而响应正在准备中。我们还将处理潜在的错误情况。
Fetch API 和 useEffect Hook处理数据获取的最常见方法之一是使用本机 Fetch API 和 useEffect Hook 的组合。
useEffect Hook 让我们在某些其他操作完成后执行副作用。借助它,我们可以跟踪应用程序何时已呈现,我们可以安全地进行 fetch 调用。
要在 Next.js 中获取数据,请将 index.js 转换为以下内容:
// 'use client' // 如果使用 /app 文件夹
import { useState, useEffect } from "react";
export default function Home() {
const [data, setData] = useState(null)
const [isLoading, setLoading] = useState(false)
useEffect(() => {
setLoading(true)
fetch('api/book')
.then((res) => res.json())
.then((data) => {
setData(data)
setLoading(false)
})
}, [])
if (isLoading) return <p>加载图书数据...</p>
if (!data) return <p>未找到图书数据</p>
return (
<div>
<h1>我最喜欢的书:</h1>
<h2>{data.title}</h2>
<p>{data.author}</p>
</div>
)
}
我们首先导入了 useState 和 useEffect hooks。然后,我们为接收到的数据设置了单独的初始状态为 null,加载时间为 false,表示没有进行 fetch 调用。
一旦应用程序已呈现,我们将加载状态设置为 true,并创建一个 fetch 调用。一旦收到响应,我们将数据设置为收到的响应,并将加载状态设置回 false,表示获取已完成。
接下来,我们需要创建一个有效的 API 端点。导航到 api 文件夹,并在其中创建一个名为 book.js 的新文件,以便在上一个代码片段中包含的 fetch 调用中包含该 API 端点。包含以下代码:
export default function handler(req, res) {
res
.status(200)
.json({ title: "追风筝的人", author: "卡勒德·胡赛尼" });
}
此代码模拟了您通常从某些外部 API 获取的书籍标题和作者的响应,但对于本教程来说,这是可以接受的。
在 Next.js 中使用 SWR 进行状态管理
Next.js 团队自己创建了另一种更方便的方法来处理数据获取。它被称为 SWR —— 一个自定义 Hook 库,处理缓存、重新验证、焦点跟踪、定时重新获取等。
要安装 SWR,请在终端中运行 npm install swr。为了看到它的效果,让我们将 index.js 文件转换为以下内容:
// 'use client' // 如果使用 /app 文件夹
import useSWR from "swr";
export default function Home() {
const fetcher = (...args) => fetch(...args).then((res) => res.json());
const { data, error } = useSWR("api/user", fetcher);
if (error) return <p>No found</p>;
if (!data) return <p>Loading...</p>;
return (
<div>
<h1>本次比赛的获胜者:</h1>
<h2>
{data.name} {data.surname}
</h2>
</div>
);
}
使用 SWR 简化了许多事情:语法看起来更清晰,更易于阅读,非常适合扩展,错误和响应状态在几行代码中得到处理。
现在,让我们创建 API 端点,以便获得响应。导航到 api 文件夹,创建一个名为 user.js 的新文件,并包含以下代码:
export default function handler(req, res) {
res.status(200).json({ name: "Jade", surname: "Summers" });
}
此 API 端点模拟了获取个人姓名和姓氏的响应,您通常会从包含公开可用名称列表的数据库或 API 获取。
Next.js 中的中间件集成
Next.js 中的中间件是一个强大的功能,可用于管理应用程序中的请求和响应。它允许您在请求完成之前运行代码,灵活地操作请求和响应,并有效地管理全局状态。
要开始使用中间件,请在 Next.js 应用程序的 /pages 或 /app 目录中创建一个 middleware.js 文件。这个文件是您将定义中间件逻辑的地方。
以下是一个基本的设置:
import { NextResponse } from 'next/server';
export function middleware(request) {
// 中间件逻辑
return NextResponse.next();
}
中间件在您需要全局管理用户身份验证状态的情况下非常强大。例如,您可以检查用户是否已经验证,并在呈现受保护路由之前将未经验证的用户重定向到登录页面。
import { NextResponse } from 'next/server';
export function middleware(request) {
const { pathname } = request.nextUrl;
// 假设您有一种方法来验证身份
if (!isUserAuthenticated() && pathname.startsWith('/protected')) {
return NextResponse.redirect('/login');
}
return NextResponse.next();
}
上面的示例确定了用户试图访问的路由,通过 request.nextUrl.pathname。然后,我们使用一个假设的 isUserAuthenticated() 代码来检查用户是否已经验证。
如果用户未经验证并尝试访问以 /protected 开头的路由,NextResponse.redirect(‘/login’) 将其重定向到登录页面。但是,如果他们被授权或访问的是非受保护路由,则中间件允许请求继续正常进行,通过 NextResponse.next()。
这种集中式方法确保身份验证状态在整个应用程序中保持一致,并减少了需要在每个受保护路由上进行身份验证的冗余代码。
中间件还可以在请求之前或之后进行一些其他处理,例如记录请求、设置响应头或缓存响应。它们是构建强大、灵活的 Next.js 应用程序的重要工具。
另一个常见的用例是根据用户偏好或系统设置管理主题设置。中间件允许您拦截请求并修改响应标头或 cookie,以反映用户的首选主题:
import { NextResponse } from 'next/server';
export function middleware(request) {
const preferredTheme = request.cookies.get('theme') || 'light';
// 修改响应以包含首选主题
const response = NextResponse.next();
response.cookies.set('theme', preferredTheme);
return response;
}
在这个例子中,我们检查 cookies 中用户的首选主题 (‘theme’),如果找不到,则默认为 ‘light’。
在发出任何请求之前,我们通过调用 response.cookies.set(‘theme’, preferredTheme) 来更新响应,将所需的主题包含在 cookies 中。现在,响应中包含了用户的主题偏好,允许您的应用程序在用户会话期间使用和管理主题状态。
Server Components 中的状态管理Next 13 引入了 Server Components,这是一种创建高性能应用程序的新方法。这些组件在初始请求期间在服务器上运行,提供诸如更快的初始页面加载和改进的 SEO 等优点。
然而,在服务器组件中进行状态管理与传统的 React 客户端组件有所不同。在深入研究这些状态管理技术之前,让我们快速看一下如何创建服务器组件以及它们与传统客户端组件的区别。
如果您使用新的 /app 目录进行路由,那么默认情况下组件将被视为服务器组件,这意味着它们在初始请求期间在服务器上运行。下面是一个从数据库查询数据并呈现的基本服务器组件示例:
import { dbConnect } from '@/services/mongo';
import TodoList from './components/TodoList';
export default async function Home() {
await dbConnect();
const todos = await dbConnect().collection('todos').find().toArray();
return (
<main>
<TodoList todos={todos} />
</main>
);
}
如上例所示,我们直接访问了服务器进程,并创建了一个异步组件。我们还能够直接从组件连接到 MongoDB 数据库,这是 Express 和 PHP 等服务器框架和语言的特点。
这就是服务器组件的亮点。然而,这也意味着我们不能在这些组件中直接使用诸如 useState() 和 useEffect() 等客户端 Hooks。
虽然服务器组件在 /app 路由中占主导地位,但您可能仍然需要与浏览器环境交互的客户端组件。要创建传统的客户端组件,只需在组件文件的顶部包含 ‘use client’ 语句,如下所示:
'use client'
import { useState} from 'react';
export default function MyClientComponent() {
const [count, setCount] = useState(0);
const handleClick = () => setCount(count + 1);
return (
<div>
<p>您点击了{count} 次</p>
<button onClick={handleClick}>+</button>
</div>
);
}
这样,您就可以像往常一样访问传统的 hooks,并可以执行客户端交互操作。
正如之前所述,服务器组件不能直接使用诸如 useState() 或 useEffect() 等 hooks,但是您可以在服务器上使用像 Zustand 这样的状态管理库。
尽管如此,通常不建议在服务器上管理复杂的应用程序状态,因为可能会导致性能问题和数据不一致性。这个 Github 讨论提供了更多相关信息。
采用服务器端存储机制,例如 cookies 或会话,用于必须在用户会话之间持久存在的状态数据。这些对于处理身份验证状态、用户偏好或不经常更改的数据非常理想。例如,您可以在服务器组件中访问用户 cookies,如下所示:
import { cookies } from 'next/headers'
export default function Page() {
const cookieStore = cookies()
return cookieStore.getAll().map((cookie) => (
<div key={cookie.name}>
<p>Name: {cookie.name}</p>
<p>Value: {cookie.value}</p>
</div>
))
}
此外,您还可以利用 Server Actions 来设置或删除 cookies,如下所示:
'use server'
import { cookies } from 'next/headers'
async function create(data)
{
cookies().set('name', 'John Doe')
}
async function delete (data) {
cookies().delete('name')
}