likes
comments
collection
share

Next.js 写什么 useState,放 URL 里!我们将讨论 “如何将状态保持在 URL 中?”,而非使用 Re

作者站长头像
站长
· 阅读数 22

前言

本篇我们将讨论 “如何将状态保持在 URL 中?”,而非使用 React 传统的 useState。

这样做的好处就在于用户分享 URL 或者刷新网页的时候不会丢失状态。当然实际场景远不止这些,网页收藏,页面前进后退,复制标签页等等场景,将状态放 URL 中都是非常有用的,而且还有利于做 SEO。

对于 Next.js 项目而言,还有一个额外的好处就是组件依然可以保持为服务端组件。这点我们最后进行讲解。

一个常见的例子就是搜索引擎,它会将搜索内容放到网址参数上:

Next.js 写什么 useState,放 URL 里!我们将讨论 “如何将状态保持在 URL 中?”,而非使用 Re

当你翻页的时候,翻页数据也会添加到页面参数上:

Next.js 写什么 useState,放 URL 里!我们将讨论 “如何将状态保持在 URL 中?”,而非使用 Re

另一个常见的例子是电商平台:

Next.js 写什么 useState,放 URL 里!我们将讨论 “如何将状态保持在 URL 中?”,而非使用 Re

可以看到:当切换商品的不同颜色或尺码的时候,网址上的 skuId 参数值在不停地变化。如果其他用户访问相同的 URL,页面就会根据 skuId 参数直接标记出对应的颜色和尺码,方便用户直接选购。

在 Next.js 中该如何实现这一功能呢?

本篇我们就以一个商品购买场景为例,为大家详细讲解如何实现。

第一步:项目初始化

为了方便演示,创建一个空的 Next.js 项目:

npx create-next-app@latest

注意勾选使用 TypeScript、Tailwind CSS、src 目录、App Router:

Next.js 写什么 useState,放 URL 里!我们将讨论 “如何将状态保持在 URL 中?”,而非使用 Re

第二步:初始样式

新建 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>
  );
}

浏览器效果如下:

Next.js 写什么 useState,放 URL 里!我们将讨论 “如何将状态保持在 URL 中?”,而非使用 Re

这里我们只是完成了页面的基础样式,交互时并无效果。

第三步:使用 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>
  );
}

此时浏览器效果如下:

Next.js 写什么 useState,放 URL 里!我们将讨论 “如何将状态保持在 URL 中?”,而非使用 Re

我们添加 "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>
  );
}

浏览器效果如下:

Next.js 写什么 useState,放 URL 里!我们将讨论 “如何将状态保持在 URL 中?”,而非使用 Re

在这一版,我们基本实现了想要的效果。当点击不同颜色或尺寸的时候,网址参数对应发生修改,当页面刷新的时候,也能正确的选中状态。

其实现原理也并不复杂,当 selectedColorselectedSize 改变的时候,使用 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>
  );
}

浏览器效果如下:

Next.js 写什么 useState,放 URL 里!我们将讨论 “如何将状态保持在 URL 中?”,而非使用 Re

在这段代码中,我们删除了原本 useStateuseEffect 相关的代码,而是改为使用 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>
  );
}

浏览器效果不变:

Next.js 写什么 useState,放 URL 里!我们将讨论 “如何将状态保持在 URL 中?”,而非使用 Re

在这段代码中,我们改为从页面函数中获取网址参数。除此之外,我们还进行了一个细小的修改,那就是 <Link> 组件的 href 链接改为使用 new URLSearchParams 的形式,使用这种方式效果会更好,因为会自动处理特殊字符,相当于加了 decodeURIComponent

而且现在页面是服务端组件,在服务端渲染,可以享受到服务端渲染的好处。

这就是最终的实现代码了。当然实际开发中,情况往往会更加复杂,不一定就能如愿使用服务端组件,Next.js 提供了 useParamsusePathnameuseRouteruseSearchParams等 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>
}

浏览器效果如下:

Next.js 写什么 useState,放 URL 里!我们将讨论 “如何将状态保持在 URL 中?”,而非使用 Re

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>
  );
}

浏览器效果如下:

Next.js 写什么 useState,放 URL 里!我们将讨论 “如何将状态保持在 URL 中?”,而非使用 Re

router 还有 pushreplacerefreshbackforward 等方法,相信大家从函数名已经知道函数的作用了。跟 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 也提供了 useParamsusePathnameuseRouteruseSearchParams等 hooks 用于获取网页参数,操作路由地址等。

转载自:https://juejin.cn/post/7399708179397787687
评论
请登录