logo
Zhanxin
Published on

巧用 Next.js URL 参数,优化状态管理

Authors

在 React 和 Next.js 的开发世界中,状态管理是构建高效应用程序的关键环节。通常,useState 是管理应用程序状态的常用方法,但随着应用的扩展,其局限性也逐渐显现。

本文将深入探讨在 Next.js 中使用 URL 参数进行状态管理的优势、用例以及实现方法,并与useState进行比较。

useState的局限性

组件范围

useState 主要用于处理特定组件范围内的状态。当需要在多个组件之间共享状态或进行全局状态管理时,可能会导致 “props drilling”(属性透传)问题,此时需要借助状态管理库,如 Redux-toolkit 或集成 useContext 钩子。

未针对 SEO优化

当 URL 参数无法反映 useState 所管理的状态调整时,可能会对 SEO 产生不利影响。

用户体验考虑

在电子商务等应用中,不使用 URL 参数可能导致用户体验不佳,因为用户无法轻松分享他们的偏好。

URL 参数的优势

网页书签

URL 参数可以将状态信息直接编码到 URL 中,方便用户为特定页面添加书签并与他人分享。

增强的状态管理

在具有搜索功能的网页中,URL 参数可以保留搜索词,即使在用户刷新浏览器后也能保持状态。

简化组件逻辑

URL 参数为各个组件提供了一种简化逻辑的方法,无需依赖useState来实现复杂的搜索功能。

理解 URL 参数查询模式

URL 参数由键值对组成,多个参数用 “&” 符号分隔。例如:

https://www.example.com/search?q=mens+t-shirt&size=3xl&color=white&sort=asc

其中,“q” 表示搜索词,后续参数如 “size”、“color” 和 “sort” 描述了其他搜索条件,增强了用户的浏览体验。

常见的 URL 参数用例

排序和过滤

使用 URL 参数,用户可以对网页内容进行排序和过滤,定制浏览体验。例如:

https://www.example.com/dresses?sort=a-z。

搜索查询

参数可以封装用户搜索查询,便于用户为搜索结果添加书签以供将来参考。例如:

https://www.example.com/search?q=t-shirt。

语言翻译

URL 参数有助于语言翻译查询,使用户能够以首选语言访问网页。例如:

https://www.example.com/news?lang=fr。

跟踪营销活动

参数可以包括活动查询,有助于跟踪点击率和活动效果。例如:

https://www.example.com/home?utm_campaign=fbid_newyearpromo&referrer_id=25jh8s。

页面分页

URL 参数有助于对网页搜索结果进行分页,确保无缝导航。例如:

https://www.example.com/blog/articles?page=3

带有 URL 的全局状态 — 优点和缺点

在管理 Web 应用程序的状态时,利用 URL 可以产生很多好处。它可以提升用户体验,促进营销活动的跟踪,并支持页面 SEO。但是,如果不小心使用,它也会给网页带来挑战。以下是一些需要考虑的优点和缺点:

优点

可添加书签和可共享的 URL:用户可以为应用程序的特定 URL 状态添加书签或与他人分享,增强了可用性和协作性。

深度链接:开发人员可以使用 URL 参数创建与查询字符串匹配的动态页面,改进应用程序状态的深度链接。

服务器端渲染 (SSR) 兼容性:对于需要服务器端渲染的项目,Next.js 与 URL 参数的结合非常理想,因为 URL 参数可以在服务器和客户端之间传输状态数据。

缺点

安全问题:存储在 URL 参数中的敏感信息可能会带来安全风险,因为它们可能对用户可见并可能被篡改。

重复内容:滥用 URL 参数会导致多个令人困惑的 URL,可能降低 SEO 引擎的页面排名。

复杂的 URL 结构:复杂的查询参数通常会导致 URL 长而难以阅读,从而阻止用户点击和信任链接,减少页面访问。

如何在 Next.js 中实现 URL 参数

创建组件

创建一个搜索输入组件 SearchSortInput,用于处理将搜索和排序查询附加到 URL。

next/navigation 导入 useRouter 和 useSearchParams 钩子。useRouter 钩子用于在客户端应用程序中进行导航,useSearchParams 钩子允许操作来自 URL 的查询。

import { useRouter, useSearchParams } from "next/navigation";

初始化钩子,并从 URL 中检索现有查询,以便在输入字段中保留任何查询。

const SearchSortInput = () => {
  const router = useRouter();
  const searchParams = useSearchParams();
  const query = searchParams?.get("q");
  const sort = searchParams?.get("sort");

  const newParams = URLSearchParams(searchParams.toString());
};

为用户创建表单以输入搜索查询,将输入的 defaultValue 设置为现有查询,利用 URL 查询参数的优势,即使用户离开或刷新页面,查询仍将存在。

return (
  <div className="flex items-center space-x-4 mb-4">
    <button
      onClick={() => router.push("/")}
      className="border border-gray-300 p-2 rounded text-black border-black"
    >
      Home
    </button>

    <form
      className="flex items-center space-x-4 mb-4 mx-auto"
    >
      <input
        type="text"
        placeholder="Search..."
        name="search"
        key={query || ""}
        defaultValue={query || ""}
        className="border border-gray-300 p-2 rounded text-black border-black"
      />
      <button
        type="submit"
        className="border border-gray-300 p-2 rounded text-black border-black"
      >
        Search
      </button>
      <div className="flex gap-2 items-center">
        <p>Sort by:</p>

        <select
          defaultValue={sort || "default"}
          name="sort"
          onChange={(e) => {
            newParams.set("sort", e.target.value);
            router.push(`/search?${newParams.toString()}`);
          }}
          className="border border-gray-300 p-2 rounded"
        >
          <option value="default">Default</option>
          <option value="title">Name</option>
          <option value="asc">Ascending</option>
          <option value="desc">Descending</option>
          <option value="a-z">A to Z</option>
        </select>
      </div>
    </form>
  </div>
);

查询处理逻辑,从表单中获取输入值。如果搜索输入有值,创建一个新查询;如果搜索输入为空,删除查询。对排序参数重复相同的过程。

最后,导航到 /search 路由并将查询参数添加到 URL。

 const handleSubmit = (event) => {
  event.preventDefault();
  const val = event.target;
  const search = val.search;
  const sortBy = val.sort;

  if (search.value) {
    newParams.set("q", search.value);
  } else {
    newParams.delete("q");
  }
  if (sortBy.value) {
    newParams.set("sort", sortBy.value);
  } else {
    newParams.delete("sort");
  }
  router.push(`/search?${newParams.toString()}`);
};

将 handleSubmit 函数添加到表单事件并导出组件

return (
  <div className="flex items-center space-x-4 mb-4">
    // other codes here...
    <form onSubmit={handleSubmit}>
    // inputs here...
    </form>
  </div>
);

export default SearchSortInput;

创建数据显示组件

创建一个接受 data、q 和 sort 参数的函数,在顶部添加 use client 指示这是一个 Next.js 客户端组件。

'use client'
const DisplayData = ({data, q, sort}) => {
 // ...
}

export default DisplayData;

创建 filteredData 函数,利用 JavaScript 的内置 filter 和 sort 方法来搜索和排序数据。如果没有搜索或排序查询,返回完整的数据。

const filteredData = () => {
  let newData = [...data];

  if (q) {
    newData = newData.filter(
      (item) =>
        item.name.toLowerCase().includes(q.toLowerCase()) ||
        item.username.toLowerCase().includes(q.toLowerCase()),
    );
  }

  if (sort) {
    newData.sort((a, b) => {
      if (sort === "name") {
        return a.name.localeCompare(b.name);
      } else if (sort === "a-z") {
        return b.username.localeCompare(a.username);
      } else if (sort === "asc") {
        return a.id - b.id;
      } else if (sort === "desc") {
        return b.id - a.id;
      } else {
        return 0;
      }
    });
  }

  return newData;
};

映射过滤后的数据并显示

return (
  <div className="flex flex-col items-center">
    <h1
      className="text-4xl font-semibold text-center mb-4 mt-8 mx-auto"
    >
      My Feed
    </h1>
    <ul className="grid grid-cols-4 mx-auto max-w-[1260px] gap-10"></ul>
    {filteredData().map((item) => (
      <ul
        key={item.id}
        className="flex border border-gray-300 p-4 rounded w-[600px] mb-4 gap-4"
      >
        <h3 className="text-lg font-semibold mb-2">{item.name}</h3>
        <p className="text-gray-500">Username: {item.username}</p>
        <p className="text-gray-500">Email: {item.email}</p>
      </ul>
    ))}
  </div>
);

创建搜索页面

在 search 文件夹中的 page.js 文件中,根据用户查询显示搜索结果。使用之前创建的 DisplayData 组件, 从 useSearchParams 获取查询参数,根据查询参数从 API 中获取数据,并使用 Suspense 处理加载状态

"use client";
import { useSearchParams } from "next/navigation";
import { Suspense, useEffect, useState } from "react";
import DisplayData from "../_components/DisplayData";
import SearchSortInput from "../_components/SearchInput";

export default function Search() {
  const searchParams = useSearchParams();
  const q = searchParams.get("q");
  const sort = searchParams.get("sort");
  const [data, setData] = useState([]);

  useEffect(() => {
    const fetchData = async () => {
      const searchParams = new URLSearchParams();

      if (q) {
        searchParams.append("q", q);
      }

      if (sort) {
        searchParams.append("sort", sort);
      }
      const response = await fetch(`/api/users`);
      const data = await response.json();

      setData(data);
    };

    fetchData();
  }, [q, sort]);

  return (
    <div className="m-12">
      <SearchSortInput />
      {q && (
        <h3 className="text-2xl font-bold mb-4">Search results for: {q}</h3>
      )}
      {sort && <p className="text-[14px] mb-4">Sorted by: {sort}</p>}
      <Suspense fallback={<div>Loading...</div>} key={q}>
        <DisplayData data={data} sort={sort} q={q} />
      </Suspense>
    </div>
  );
}

在主页的 page.js 文件中,显示 SearchInput 和 DisplayData 组件,从 API 路由中获取数据并显示组件。

"use client";
import { Suspense, useEffect, useState } from "react";
import DisplayData from "./_components/DisplayData";
import SearchSortInput from "./_components/SearchInput";

export default function Home() {
  const [data, setData] = useState([]);
  const fetchPosts = async () => {
    const res = await fetch("/api/users");
    const data = await res.json();
    setData(data);
  };

  useEffect(() => {
    fetchPosts();
  }, []);
  return (
    <div className="m-12">
      <SearchSortInput />
      <Suspense fallback={<div>Loading...</div>}>
        <DisplayData data={data} />
      </Suspense>
    </div>
  );
}

通过以上步骤,在 Next.js 中实现了使用 URL 参数进行状态管理。