Next.js 写什么 useState,放 URL 里!我们将讨论 “如何将状态保持在 URL 中?”,而非使用 Re
前言
本篇我们将讨论 “如何将状态保持在 URL 中?”,而非使用 React 传统的 useState。
这样做的好处就在于用户分享 URL 或者刷新网页的时候不会丢失状态。当然实际场景远不止这些,网页收藏,页面前进后退,复制标签页等等场景,将状态放 URL 中都是非常有用的,而且还有利于做 SEO。
对于 Next.js 项目而言,还有一个额外的好处就是组件依然可以保持为服务端组件。这点我们最后进行讲解。
一个常见的例子就是搜索引擎,它会将搜索内容放到网址参数上:
当你翻页的时候,翻页数据也会添加到页面参数上:
另一个常见的例子是电商平台:
可以看到:当切换商品的不同颜色或尺码的时候,网址上的 skuId
参数值在不停地变化。如果其他用户访问相同的 URL,页面就会根据 skuId 参数直接标记出对应的颜色和尺码,方便用户直接选购。
在 Next.js 中该如何实现这一功能呢?
本篇我们就以一个商品购买场景为例,为大家详细讲解如何实现。
第一步:项目初始化
为了方便演示,创建一个空的 Next.js 项目:
npx create-next-app@latest
注意勾选使用 TypeScript、Tailwind CSS、src 目录、App Router:
第二步:初始样式
新建 src/app/product/[id]/page.tsx
,代码如下:
/* eslint-disable @next/next/no-img-element */
const colors = [
["white", "漂白"],
["blue", "蔚蓝"],
["green", "军绿"],
["gray", "浅灰"],
];
const sizes = ["s", "m", "l", "xl", "2xl", "3xl"];
export default function Page() {
const selectedColor = "white";
const selectedSize = "s";
return (
<div className="flex flex-row bg-white w-full justify-center text-slate-800 gap-4 pt-10">
<div>
<img
src="https://tailwindui.com/img/ecommerce-images/product-page-02-secondary-product-shot.jpg"
alt="Two each of gray, white, and black shirts laying flat."
className="w-[200px]"
/>
</div>
<div className="flex flex-col gap-4 justify-center">
<h2>颜色:</h2>
<div>
{colors.map(([value, name], index) => {
return (
<button
key={index}
className={`px-2 py-1 rounded-md mx-2 border-2 ${
selectedColor == value
? "border-amber-500"
: "border-gray-600"
}`}
>
{name}
</button>
);
})}
</div>
<h2>尺寸:</h2>
<div>
{sizes.map((value, index) => {
return (
<button
key={index}
className={`px-2 py-1 rounded-md mx-2 border-2 ${
selectedSize == value ? "border-amber-500" : "border-gray-600"
}`}
>
{value.toUpperCase()}
</button>
);
})}
</div>
</div>
</div>
);
}
浏览器效果如下:
这里我们只是完成了页面的基础样式,交互时并无效果。
第三步:使用 useState
我们先用 useState 实现一版,修改 src/app/product/[id]/page.tsx
,完整代码如下:
"use client";
import { useState } from "react";
/* eslint-disable @next/next/no-img-element */
const colors = [
["white", "漂白"],
["blue", "蔚蓝"],
["green", "军绿"],
["gray", "浅灰"],
];
const sizes = ["s", "m", "l", "xl", "2xl", "3xl"];
export default function Page() {
const [selectedColor, setSelectedColor] = useState("white");
const [selectedSize, setSelectedSize] = useState("s");
return (
<div className="flex flex-row bg-white w-full justify-center text-slate-800 gap-4 pt-10">
<div>
<img
src="https://tailwindui.com/img/ecommerce-images/product-page-02-secondary-product-shot.jpg"
alt="Two each of gray, white, and black shirts laying flat."
className="w-[200px]"
/>
</div>
<div className="flex flex-col gap-4 justify-center">
<h2>颜色:</h2>
<div>
{colors.map(([value, name], index) => {
return (
<button
key={index}
className={`px-2 py-1 rounded-md mx-2 border-2 ${
selectedColor == value
? "border-amber-500"
: "border-gray-600"
}`}
onClick={() => {
setSelectedColor(value);
}}
>
{name}
</button>
);
})}
</div>
<h2>尺寸:</h2>
<div>
{sizes.map((value, index) => {
return (
<button
key={index}
className={`px-2 py-1 rounded-md mx-2 border-2 ${
selectedSize == value ? "border-amber-500" : "border-gray-600"
}`}
onClick={() => {
setSelectedSize(value);
}}
>
{value.toUpperCase()}
</button>
);
})}
</div>
</div>
</div>
);
}
此时浏览器效果如下:
我们添加 "use client"
指令,将页面改为客户端组件,并使用 useState 设置选择的颜色和尺寸。可以看到,点击的时候,已经能够正常显示选中状态,但页面刷新的时候,状态会重置。
第四步:同步网页参数
目前,我们只是显示了正确的选中状态,但状态并没有同步到网址参数上,这一版我们来实现一下。继续修改 src/app/product/[id]/page.tsx
,完整代码如下:
"use client";
import { useEffect, useState } from "react";
/* eslint-disable @next/next/no-img-element */
const colors = [
["white", "漂白"],
["blue", "蔚蓝"],
["green", "军绿"],
["gray", "浅灰"],
];
const sizes = ["s", "m", "l", "xl", "2xl", "3xl"];
export default function Page() {
const [selectedColor, setSelectedColor] = useState("");
const [selectedSize, setSelectedSize] = useState("");
useEffect(() => {
if (selectedColor && selectedSize) {
window.history.pushState(
null,
"",
`?color=${selectedColor}&size=${selectedSize}`
);
}
}, [selectedColor, selectedSize]);
useEffect(() => {
const searchParams = new URLSearchParams(window.location.search);
setSelectedColor(searchParams.get("color") || "white");
setSelectedSize(searchParams.get("size") || "s");
}, []);
return (
<div className="flex flex-row bg-white w-full justify-center text-slate-800 gap-4 pt-10">
<div>
<img
src="https://tailwindui.com/img/ecommerce-images/product-page-02-secondary-product-shot.jpg"
alt="Two each of gray, white, and black shirts laying flat."
className="w-[200px]"
/>
</div>
<div className="flex flex-col gap-4 justify-center">
<h2>颜色:</h2>
<div>
{colors.map(([value, name], index) => {
return (
<button
key={index}
className={`px-2 py-1 rounded-md mx-2 border-2 ${
selectedColor == value
? "border-amber-500"
: "border-gray-600"
}`}
onClick={() => {
setSelectedColor(value);
}}
>
{name}
</button>
);
})}
</div>
<h2>尺寸:</h2>
<div>
{sizes.map((value, index) => {
return (
<button
key={index}
className={`px-2 py-1 rounded-md mx-2 border-2 ${
selectedSize == value ? "border-amber-500" : "border-gray-600"
}`}
onClick={() => {
setSelectedSize(value);
}}
>
{value.toUpperCase()}
</button>
);
})}
</div>
</div>
</div>
);
}
浏览器效果如下:
在这一版,我们基本实现了想要的效果。当点击不同颜色或尺寸的时候,网址参数对应发生修改,当页面刷新的时候,也能正确的选中状态。
其实现原理也并不复杂,当 selectedColor
和 selectedSize
改变的时候,使用 window.history.pushState
修改网址参数。当页面刷新的时候,从网址参数上获取初始状态。相信大家都能实现。
第五步:使用 Next.js
到现在其实跟 Next.js 并没有什么关系,因为在纯 React 项目中,也可以这样写。但其实 Next.js 提供了丰富的 API 可以帮助我们实现相同的功能。继续修改 src/app/product/[id]/page.tsx
,完整代码如下:
"use client";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
/* eslint-disable @next/next/no-img-element */
const colors = [
["white", "漂白"],
["blue", "蔚蓝"],
["green", "军绿"],
["gray", "浅灰"],
];
const sizes = ["s", "m", "l", "xl", "2xl", "3xl"];
export default function Page() {
const searchParams = useSearchParams();
const selectedColor = searchParams.get("color");
const selectedSize = searchParams.get("size");
return (
<div className="flex flex-row bg-white w-full justify-center text-slate-800 gap-4 pt-10">
<div>
<img
src="https://tailwindui.com/img/ecommerce-images/product-page-02-secondary-product-shot.jpg"
alt="Two each of gray, white, and black shirts laying flat."
className="w-[200px]"
/>
</div>
<div className="flex flex-col gap-4 justify-center">
<h2>颜色:</h2>
<div>
{colors.map(([value, name], index) => {
return (
<Link
key={index}
href={`?color=${value}&&size=${selectedSize}`}
className={`px-2 py-1 rounded-md mx-2 border-2 ${
selectedColor == value
? "border-amber-500"
: "border-gray-600"
}`}
>
{name}
</Link>
);
})}
</div>
<h2>尺寸:</h2>
<div>
{sizes.map((size, index) => {
return (
<Link
key={index}
href={`?color=${selectedColor}&&size=${size}`}
className={`px-2 py-1 rounded-md mx-2 border-2 ${
selectedSize == size ? "border-amber-500" : "border-gray-600"
}`}
>
{size.toUpperCase()}
</Link>
);
})}
</div>
</div>
</div>
);
}
浏览器效果如下:
在这段代码中,我们删除了原本 useState
、useEffect
相关的代码,而是改为使用 Next.js 提供的 useSearchParams
hook 获取网页参数。同时为了方便同步网页参数,我们将原本的 <button>
组件改为了 <Link>
组件。
整体代码更加精简,但我们却实现了更好的效果。在之前的实现中,页面刷新后,会有一小段没有选中状态的时间,然后才进入选中状态。而在现在的实现中,页面刷新后,直接就是选中状态,效果更加自然。
第六步:保持服务端组件
目前我们使用了 useSearchParams
hook 获取网页参数,但使用该 hook 必须是在客户端组件中,所以我们使用了 "use client"
指令,但其实完全可以不用这个 hook,因为 Next.js 的 Page 完全可以直接获取页面参数。现在我们修改 src/app/product/[id]/page.tsx
,完整代码如下:
import Link from "next/link";
/* eslint-disable @next/next/no-img-element */
const colors = [
["white", "漂白"],
["blue", "蔚蓝"],
["green", "军绿"],
["gray", "浅灰"],
];
const sizes = ["s", "m", "l", "xl", "2xl", "3xl"];
export default function Page({
params,
searchParams,
}: {
params: { slug: string };
searchParams: { [key: string]: string | string[] | undefined };
}) {
const selectedColor = (searchParams.color || "white") as string;
const selectedSize = (searchParams.size || "s") as string;
return (
<div className="flex flex-row bg-white w-full justify-center text-slate-800 gap-4 pt-10">
<div>
<img
src="https://tailwindui.com/img/ecommerce-images/product-page-02-secondary-product-shot.jpg"
alt="Two each of gray, white, and black shirts laying flat."
className="w-[200px]"
/>
</div>
<div className="flex flex-col gap-4 justify-center">
<h2>颜色:</h2>
<div>
{colors.map(([color, name], index) => {
return (
<Link
key={index}
href={`?${new URLSearchParams({
color,
size: selectedSize,
})}`}
className={`px-2 py-1 rounded-md mx-2 border-2 ${
selectedColor == color
? "border-amber-500"
: "border-gray-600"
}`}
>
{name}
</Link>
);
})}
</div>
<h2>尺寸:</h2>
<div>
{sizes.map((size, index) => {
return (
<Link
key={index}
href={`?${new URLSearchParams({
color: selectedColor,
size,
})}`}
className={`px-2 py-1 rounded-md mx-2 border-2 ${
selectedSize == size ? "border-amber-500" : "border-gray-600"
}`}
>
{size.toUpperCase()}
</Link>
);
})}
</div>
</div>
</div>
);
}
浏览器效果不变:
在这段代码中,我们改为从页面函数中获取网址参数。除此之外,我们还进行了一个细小的修改,那就是 <Link>
组件的 href 链接改为使用 new URLSearchParams
的形式,使用这种方式效果会更好,因为会自动处理特殊字符,相当于加了 decodeURIComponent
。
而且现在页面是服务端组件,在服务端渲染,可以享受到服务端渲染的好处。
这就是最终的实现代码了。当然实际开发中,情况往往会更加复杂,不一定就能如愿使用服务端组件,Next.js 提供了 useParams
、usePathname
、useRouter
、useSearchParams
等 hooks 用于获取网页参数,操作路由地址等。
常用 hooks
注意使用这些 hooks 都只能在客户端组件中使用。其中:
useParams
用于获取网页动态参数,useSearchParams
用于获取网页搜索参数。这两个参数其实可以改为从页面函数中获取,这样也许可以避免使用客户端组件:
// app/blog/[slug]/page.tsx
export default function Page({
params,
searchParams,
}: {
params: { slug: string }
searchParams: { [key: string]: string | string[] | undefined }
}) {
return <h1>My Page</h1>
}
usePathname
用于获取网页地址:
'use client'
import { usePathname } from 'next/navigation'
export default function ExampleClientComponent() {
const pathname = usePathname()
return <p>Current pathname: {pathname}</p>
}
浏览器效果如下:
useRouter
用的频率更多,主要用于操作路由:
"use client";
import { useRouter } from "next/navigation";
export default function Page() {
const router = useRouter();
return (
<button type="button" onClick={() => router.push("/product/dashboard")}>
Dashboard
</button>
);
}
浏览器效果如下:
router 还有 push
、replace
、refresh
、back
、forward
等方法,相信大家从函数名已经知道函数的作用了。跟 window.history
的 API 相互对应。
使用 router 操作路由的时候,有的时候需要注意 scroll 滚动距离。默认情况下,当导航至新路由的时候,会滚动到页面顶部,你可以传递 { scroll: false }
参数禁用这个操作:
'use client'
import { useRouter } from 'next/navigation'
export default function Page() {
const router = useRouter()
return (
<button
type="button"
onClick={() => router.push('/dashboard', { scroll: false })}
>
Dashboard
</button>
)
}
如果你需要监听路由地址的变化,做一些响应,你可以这样做:
'use client'
import { useEffect } from 'react'
import { usePathname, useSearchParams } from 'next/navigation'
export function NavigationEvents() {
const pathname = usePathname()
const searchParams = useSearchParams()
useEffect(() => {
const url = `${pathname}?${searchParams}`
console.log(url)
// ...
}, [pathname, searchParams])
return null
}
基本上,将状态放到 URL 中并保持网页参数与状态同步可能会用到的 API 就这些。实际情况会比较复杂,请选择合适的 API 进行使用。
总结
将状态放到 URL 中,可以在页面刷新等场景中继续保持状态,有利于 SEO、且可以让组件继续保持为服务端组件,好处多多。在具体实现的时候,可以从页面函数中获取网页参数, 使用<Link>
组件将状态同步到网址上。同步的时候,应该尽可能使用 new URLSearchParams
的形式,它会自动处理特殊字符。
如果需要使用客户端组件,Next.js 也提供了 useParams
、usePathname
、useRouter
、useSearchParams
等 hooks 用于获取网页参数,操作路由地址等。
转载自:https://juejin.cn/post/7399708179397787687