构建 Vite + React-Router6 实例

案例最终效果图

需要实现的功能如目录所示

1.安装|Setup

npm create vite@latest my-project --template react













cd my-project

npm install react-router-dom localforage match-sorter sort-by








npm run dev

打开终端上的 URL:

 VITE v4.3.9  ready in 678 ms













  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h to show help

? 复制/粘贴这里的CSS到 src/index.css

? 复制/粘贴这里的模拟接口到 src/contacts.js

? 删除 src/中不使用的文件,此时的目录如下:

src
├── contacts.js
├── index.css
└── main.jsx

2.初始化第一个路由

修改入口文件 main.jsx 如下:

import React from "react";
import ReactDOM from "react-dom/client";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import "./index.css";





const router = createBrowserRouter([
  {
    path: "/",
    element: <div>Hello world!</div>,
  },
]);



ReactDOM.createRoot(document.getElementById("root")).render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>
);

初始化后调试如下图

3.初始化根路由模板

? 创建文件夹 src/routes
? 创建根布局组件 src/routes/root.jsx

export default function Root() {
  return (

    <>
      <div id="sidebar">
        <h1>React Router Contacts</h1>
        <div>
          <form id="search-form" role="search">
            <input
              id="q"
              aria-label="Search contacts"
              placeholder="Search"
              type="search"
              name="q"
            />
            <div id="search-spinner" aria-hidden hidden={true} />
            <div className="sr-only" aria-live="polite"></div>
          </form>
          <form method="post">
            <button type="submit">New</button>
          </form>
        </div>
        <nav>
          <ul>
            <li>
              <a href={`/contacts/1`}>Your Name</a>
            </li>
            <li>
              <a href={`/contacts/2`}>Your Friend</a>
            </li>
          </ul>
        </nav>
      </div>
      <div id="detail"></div>
    </>
  );
}

将新增的 组件引入 main.jsx 如下:

import Root from "./routes/root";














const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
  },

]);

最终调式结果如下
图片.png

4.初始化错误提示模板

? 创建错误页面组件 src/error-page.jsx
并使用useRouteError 获取错误响应的提示

import { useRouteError } from "react-router-dom";













export default function ErrorPage() {
  const error = useRouteError();
  console.error(error);








  return (

    <div id="error-page">
      <h1>Oops!</h1>
      <p>Sorry, an unexpected error has occurred.</p>
      <p>

        <i>{error.statusText || error.message}</i>
      </p>
    </div>
  );
}

在入口文件 main.jsx 中引入错误提示组件

import Root from "./routes/root";

import ErrorPage from "./error-page";
















const router = createBrowserRouter([







  {







    path: "/",







    element: <Root />,






    errorElement: <ErrorPage />,






  },
]);

错误页调试结果如下
图片.png

5.初始化嵌套路由

为了实现点击左侧边栏菜单组件时,显示对应页面内容。需要使用 createBrowserRouter 的子路由属性children
? 创建联系人路由模块

import { Form } from "react-router-dom";













export default function Contact() {
  const contact = {
    first: "Your",
    last: "Name",
    avatar: "https://placekitten.com/g/200/200",
    twitter: "your_handle",
    notes: "Some notes",
    favorite: true,
  };



  return (


    <div id="contact">
      <div>
        <img key={contact.avatar} src={contact.avatar || null} />
      </div>

      <div>
        <h1>
          {contact.first || contact.last ? (
            <>
              {contact.first} {contact.last}
            </>
          ) : (
            <i>No Name</i>
          )}{" "}
          <Favorite contact={contact} />
        </h1>

        {contact.twitter && (
          <p>
            <a target="_blank" href={`https://twitter.com/${contact.twitter}`}>
              {contact.twitter}
            </a>
          </p>
        )}


        {contact.notes && <p>{contact.notes}</p>}

        <div>
          <Form action="edit">
            <button type="submit">Edit</button>
          </Form>
          <Form
            method="post"
            action="destroy"
            onSubmit={event => {
              if (!confirm("Please confirm you want to delete this record.")) {
                event.preventDefault();
              }
            }}
          >
            <button type="submit">Delete</button>
          </Form>
        </div>
      </div>
    </div>
  );
}

function Favorite({ contact }) {
  // yes, this is a `let` for later
  let favorite = contact.favorite;
  return (
    <Form method="post">
      <button
        name="favorite"
        value={favorite ? "false" : "true"}
        aria-label={favorite ? "Remove from favorites" : "Add to favorites"}
      >
        {favorite ? "★" : "☆"}
      </button>
    </Form>
  );
}

修改如果入口文件 main.jsx 如下

/* existing imports */
import Contact from "./routes/contact";
















const router = createBrowserRouter([







  {







    path: "/",







    element: <Root />,






    errorElement: <ErrorPage />,






    children: [
      {

        path: "contacts/:contactId",
        element: <Contact />,
      },

    ],


  },


]);


此时还没有完成配置,还需要在根路由 src/routes/root.jsx 中引入 属性实现嵌套的效果

import { Outlet } from "react-router-dom";













export default function Root() {


  return (

    <>

      {/* all the other elements */}
      <div id="detail">
        <Outlet />
      </div>
    </>
  );
}

嵌套路由最终调试结果

6.实现路由懒加载

这个时候 F12 查看 Network 请求,你会发现我们点击侧边栏中的链接时,浏览器会对下一个 URL 进行完整的文档请求,而不是使用 React Router。
客户端路由可以让应用程序更新 URL,不需要从服务器请求另一个文档。

相反,用**<Link/>**属性可以实现它立即呈现新的 UI

import { Outlet, Link } from "react-router-dom";














export default function Root() {


  return (

    <>

      <div id="sidebar">
        {/* other elements */}





        <nav>
          <ul>
            <li>
              <Link to={`contacts/1`}>Your Name</Link>
            </li>
            <li>
              <Link to={`contacts/2`}>Your Friend</Link>
            </li>
          </ul>
        </nav>

        {/* other elements */}
      </div>
    </>
  );

}

虽然目前已经实现了无需请求服务,就能即呈现新的 UI 的路由。
但是目前的 To 参数还是写死的,这其中还存在耦合性,为了解决这个问题官方采购了异步懒加载请求路由的方法。结合loader 属性和 useLoaderData 方法完成

? 在 src/routes/root.jsx 中导出一个加载器

import { Outlet, Link } from "react-router-dom";

import { getContacts } from "../contacts";

















export async function loader() {
  const contacts = await getContacts();
  return { contacts };
}




? 并在入口文件 main.jsx 中引用

/* other imports */

import Root, { loader as rootLoader } from "./routes/root";
















const router = createBrowserRouter([







  {







    path: "/",







    element: <Root />,






    errorElement: <ErrorPage />,






    loader: rootLoader,




    children: [
      {
        path: "contacts/:contactId",
        element: <Contact />,
      },
    ],
  },
]);

? 最后将写死的路由导航 to 参数修改为动态的形式

import { Outlet, Link, useLoaderData } from "react-router-dom";
import { getContacts } from "../contacts";

















/* other code */





export default function Root() {
  const { contacts } = useLoaderData();
  return (
    <>
      <div id="sidebar">
        <h1>React Router Contacts</h1>
        {/* other code */}



        <nav>
          {contacts.length ? (
            <ul>
              {contacts.map(contact => (
                <li key={contact.id}>
                  <Link to={`contacts/${contact.id}`}>
                    {contact.first || contact.last ? (
                      <>
                        {contact.first} {contact.last}
                      </>
                    ) : (
                      <i>No Name</i>
                    )}{" "}
                    {contact.favorite && <span>★</span>}
                  </Link>
                </li>
              ))}
            </ul>
          ) : (
            <p>
              <i>No contacts</i>
            </p>
          )}
        </nav>


        {/* other code */}
      </div>
    </>

  );

}

修改为动态写入菜单栏数据之后,现在会自动将这些数据与你的 UI 保持同步。但我们还没有任何数据,所以你的菜单栏可能会得到这样一个空白列表:

图片.png

7.实现动态路由

模仿 forms 表单导航的模型,可以实现一个简单的客户端渲染功能。 forms 表单实际上会在浏览器中引起导航,就像单击”<a/>”链接一样。唯一的区别在于请求:链接只能更改 URL,而 forms 表单可以更改请求方法(GET vs POST)和请求主体(POST 表单数据)。

如果没有客户端路由,浏览器将自动序列化表单的数据,并将其作为 POST 的请求主体发送到服务器。React Router 也做同样的事情,将表单的数据作为 GET 的 URLSearchParams 发送到服务器。只不过它不是把请求发送给服务器,而是使用客户端路由,把它发送给一个路由属性’action‘。

具体实现:
在 src/routes/root.jsx 中导出一个 action ,将它连接到路由配置中,并将更改为 React Router < Form>,从而创建新的联系人。

? 创建 action 并将更改为

import { Outlet, Link, useLoaderData, Form } from "react-router-dom";
import { getContacts, createContact } from "../contacts";

















export async function action() {

  const contact = await createContact();

  return { contact };

}









/* other code */




export default function Root() {
  const { contacts } = useLoaderData();
  return (


    <>
      <div id="sidebar">
        <h1>React Router Contacts</h1>
        <div>
          {/* other code */}
          <Form method="post">
            <button type="submit">New</button>
          </Form>
        </div>

        {/* other code */}
      </div>
    </>
  );

}

? 在入口文件 maib.jsx 上导入并设置 action 属性

/* other imports */














import Root, {
  loader as rootLoader,
  action as rootAction,
} from "./routes/root";



const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    errorElement: <ErrorPage />,
    loader: rootLoader,
    action: rootAction,
    children: [
      {

        path: "contacts/:contactId",

        element: <Contact />,

      },
    ],
  },
]);

这时点击新增按钮 New 你会发现菜单栏已经有 No Name 数据被添加进来了
图片.png
到这里已经完成菜单栏的动态加入表单路由,但目前点击 新增的表单 No Name 发现右边显示的数据并不一样。
图片.png
为了同步右边的数据,我们还需要完成以下步骤:
? 在 src/routes/contact.jsx 页面添加加载器loader,并改用 useLoaderData 方法来访问(读取)数据

import { Form, useLoaderData } from "react-router-dom";

import { getContact } from "../contacts";
















export async function loader({ params }) {
  const contact = await getContact(params.contactId);
  return { contact };

}









export default function Contact() {
  const { contact } = useLoaderData();
  // existing code
}

? 在入口文件 main.jsx 上引入并改子路由配置加载器

/* existing code */



import Contact, { loader as contactLoader } from "./routes/contact";
















const router = createBrowserRouter([







  {







    path: "/",







    element: <Root />,






    errorElement: <ErrorPage />,






    loader: rootLoader,




    action: rootAction,



    children: [



      {


        path: "contacts/:contactId",


        element: <Contact />,


        loader: contactLoader,


      },


    ],
  },
]);

/* existing code */

再次点击菜单栏中的 No Name 表单,显示结果如下
图片.png到这里手动新增的动态路由功能已经实现。但了实现不同菜单栏对应不同的内容,我们还需要一个手动修改右侧内容的路由。

8.实现新增编辑路由

同理新增一个路由组件,并将其引入根路由的指定位置
? 在 src/routes/edit.jsx 创建编辑路由组件

import { Form, useLoaderData } from "react-router-dom";














export default function EditContact() {

  const { contact } = useLoaderData();






  return (
    <Form method="post" id="contact-form">
      <p>
        <span>Name</span>
        <input
          placeholder="First"
          aria-label="First name"
          type="text"
          name="first"
          defaultValue={contact.first}
        />
        <input
          placeholder="Last"
          aria-label="Last name"
          type="text"
          name="last"
          defaultValue={contact.last}
        />

      </p>
      <label>
        <span>Twitter</span>
        <input
          type="text"
          name="twitter"
          placeholder="@jack"
          defaultValue={contact.twitter}
        />
      </label>
      <label>
        <span>Avatar URL</span>
        <input
          placeholder="https://example.com/avatar.jpg"
          aria-label="Avatar URL"
          type="text"
          name="avatar"
          defaultValue={contact.avatar}
        />
      </label>
      <label>
        <span>Notes</span>
        <textarea name="notes" defaultValue={contact.notes} rows={6} />
      </label>
      <p>
        <button type="submit">Save</button>
        <button type="button">Cancel</button>
      </p>
    </Form>
  );
}

? 在入口文件 main.jsx 中添加新的编辑路由

/* existing code */



import EditContact from "./routes/edit";
















const router = createBrowserRouter([







  {







    path: "/",







    element: <Root />,






    errorElement: <ErrorPage />,






    loader: rootLoader,




    action: rootAction,



    children: [



      {


        path: "contacts/:contactId",


        element: <Contact />,


        loader: contactLoader,


      },


      {

        path: "contacts/:contactId/edit",

        element: <EditContact />,

        loader: contactLoader,

      },


    ],

  },

]);


/* existing code */

点击右侧内容的Edit编辑按钮,最终调试结果如下
图片.png到这里新增的路由已经添加完成。
但为了能将填入的信息更新记录,我们需要将更新的路由参数通过 action 属性连接到根路由上。
当表单发送操作,数据将自动重新验证。

? 向编辑组件添加一个更新操作

import { Form, useLoaderData, redirect } from "react-router-dom";
import { updateContact } from "../contacts";
















export async function action({ request, params }) {

  const formData = await request.formData();
  const updates = Object.fromEntries(formData);
  await updateContact(params.contactId, updates);
  return redirect(`/contacts/${params.contactId}`);
}





/* existing code */

? 在入口文件 main.jsx 中引入修改的 action 属性中

/* existing code */



import EditContact, { action as editAction } from "./routes/edit";
















const router = createBrowserRouter([







  {







    path: "/",







    element: <Root />,






    errorElement: <ErrorPage />,






    loader: rootLoader,




    action: rootAction,



    children: [



      {


        path: "contacts/:contactId",


        element: <Contact />,


        loader: contactLoader,


      },


      {

        path: "contacts/:contactId/edit",

        element: <EditContact />,

        loader: contactLoader,

        action: editAction,
      },
    ],

  },

]);

/* existing code */

填入信息之后保存调试结果如下
图片.png到这里编辑更新信息的功能已经实现,但是我们不知道这里发生了什么…

9.实现路由重定向

下面让我们深入的分析一下它究竟是怎么工作的。

读取输入参数

打开 src/routes/edit.jsx,看看表单元素。注意它们每个都有一个 name:

<input
  placeholder="First"
  aria-label="First name"
  type="text"
  name="first"
  defaultValue={contact.first}
/>

如果没有 JavaScript,当提交表单时,浏览器会创建 formdata,并在将其发送到服务器时,设置为请求的主体。
如前所述,React Router <Link/>会阻止这种情况,并将请求发送给对应根路由的 action 。而** FormData**表单也是一样的道理,它的每个字段都可以通过 formData.get(name) 来访问。例如,给定上面的输入字段,你可以通过访问 name 属性读取到对应的 first 名称:

export async function action({ request, params }) {

  const formData = await request.formData();

  const firstName = formData.get("first");
  const lastName = formData.get("last");
  // ...
}


转换成对象参数
之后将读取到的参数,通过Object.fromEntries 方法将数组对象转化为对象

将参数转给调用接口同时更新路由参数
更新之后通过 redirect 将路由参数从编辑路由从定向到客户端路由

export async function action({ request, params }) {

  const formData = await request.formData();

  const updates = Object.fromEntries(formData);
  await updateContact(params.contactId, updates);
  return redirect(`/contacts/${params.contactId}`);
}


在新增的时候,如果菜单列表中没有客户路由,额外的重新验证代码就不存在,所以它也不需要在客户端路由中存在。所以在新增的客户路由时,直接将新增的表单重定向到信息编辑页面即可

? 在 sec/routes/root.jsx 中添加重定向到新记录的编辑页的 redirect 属性

import { Outlet, Link, useLoaderData, Form, redirect } from "react-router-dom";
import { getContacts, createContact } from "../contacts";

















export async function action() {

  const contact = await createContact();

  return redirect(`/contacts/${contact.id}/edit`);
}




现在点击 New 按钮新增重定向后的调试效果如下
图片.png到这里新增路由重定向的功能已经实现。但有个问题当选择菜单路由时,没有很明显的标注样式,所以不知道当前的选择是谁。

10.实现选中的菜单挂载

为了解决这个问题,可以将跟路由的 src/routes/root.jsx 中的 <Link/> 换成 <NavLink />NavLink

import {



  Outlet,
  NavLink,
  useLoaderData,
  Form,
  redirect,
} from "react-router-dom";





export default function Root() {
  return (
    <>
      <div id="sidebar">
        {/* other code */}

        <nav>
          {contacts.length ? (
            <ul>
              {contacts.map(contact => (
                <li key={contact.id}>
                  <NavLink
                    to={`contacts/${contact.id}`}
                    className={({ isActive, isPending }) =>
                      isActive ? "active" : isPending ? "pending" : ""
                    }
                  >
                    {/* other code */}
                  </NavLink>
                </li>
              ))}
            </ul>
          ) : (
            <p>{/* other code */}</p>
          )}
        </nav>
      </div>
    </>

  );

}

使用 NavLink 属性后可以很清除的看见当前所在位置,但你在切换不同菜单导航时,你会发现感觉反应有点迟钝。

11.实现全局响应挂起

切换菜单导航迟钝的原因:
当用户浏览应用程序时,React 路由器会保留旧页面,因为数据正在加载到下一页。

解决方法:
在数据加载完成之前保持原来在状态,加载完成之后在路由在跳。为此官方封装了一个方法useNavigation

idle → submitting → loading → idle

在 src/routes/root.jsx 中引入 useNavigation

import {



  // existing code
  useNavigation,
} from "react-router-dom";





// existing code



export default function Root() {
  const { contacts } = useLoaderData();
  const navigation = useNavigation();


  return (

    <>

      <div id="sidebar">{/* existing code */}</div>
      <div
        id="detail"
        className={navigation.state === "loading" ? "loading" : ""}
      >
        <Outlet />
      </div>
    </>
  );
}

通过useNavigation方法,解决了 UI 界面响应迟钝的问题。

12.实现删除路由记录

实现原理:
通过 <Form/> 的 action 属性读取到的相关标记值。
在相对操作将把表单提交给删除路由 contact/:contactId/destroy。

当用户点击提交按钮时:

  1. react-router-dom 的 <Form>方法阻止了浏览器默认向服务器发送新的 POST 请求的行为,而是通过使用客户端路由创建 POST 请求来模拟浏览器
  2. <Form action="destroy">匹配到根路由下的"contacts/:contactId/destroy"路径,并向它发送请求
  3. action 重定向后,删除记录后,通过重定向路由。让 React Router 调用页面上所有数据的加载器来获取最新的值,使用useLoaderData读取返回新值并更新到组件!

? 创建“destroy”路由模块 src/routes/destroy.jsx

import { redirect } from "react-router-dom";
import { deleteContact } from "../contacts";
















export async function action({ params }) {
  await deleteContact(params.contactId);
  return redirect("/");
}




? 在路由配置中添加 destroy 路由

/* existing code */



import { action as destroyAction } from "./routes/destroy";
















const router = createBrowserRouter([







  {







    path: "/",







    /* existing root route props */
    children: [

      /* existing routes */
      {

        path: "contacts/:contactId/destroy",
        action: destroyAction,
      },

    ],


  },


]);




/* existing code */

? 当操作抛出一个错误信息

export async function action({ params }) {
  throw new Error("oh dang!");
}

如果在子路由:

  • 未添加 errorElement 属性时,他会找到根路由的 errorElement
  • 添加有 errorElement 属性时,他会找到字节自己的 errorElement(冒泡就近原则)

? 配置路由errorElement属性

[
  /* other routes */
  {
    path: "contacts/:contactId/destroy",
    action: destroyAction,
    errorElement: <div>Oops! There was an error.</div>,
  },

];

删除与抛出错误信息的最终调试如下
图片.png删除与抛出错误信息时,信息对应的功能已经实现。但还有一个问题,当返回根路由时,右侧的内容时空白的。

13.实现默认子路由模板

当一个路由有子路由,而你在父路由的路径上时(http://localhost:5173/ ),根路由上的 <Outlet>属性没有什么要渲染的内容,因为没有匹配的子路由。所以右侧内容显示空白页。
为了解决这个问题,您可以将索引路由视为默认的子路由来填充该空间。

? 创建索引路由模块

export default function Index() {
  return (

    <p id="zero-state">
      This is a demo for React Router.
      








      Check out{" "}
      <a href="https://reactrouter.com">the docs at reactrouter.com</a>.
    </p>
  );
}

? 配置索引路由

// existing code






import Index from "./routes/index";
















const router = createBrowserRouter([







  {







    path: "/",







    element: <Root />,






    errorElement: <ErrorPage />,






    loader: rootLoader,




    action: rootAction,



    children: [



      { index: true, element: <Index /> },
      /* existing routes */
    ],


  },


]);


最终调试结果如下
图片.png

14.实现撤回路由

在编辑页面上,我们有一个取消按钮,它还没有做任何事情。我们希望它和浏览器的后退按钮做同样的事情。

? 使用 useNavigate 添加取消按钮单击处理程序

import { Form, useLoaderData, redirect, useNavigate } from "react-router-dom";













export default function EditContact() {

  const { contact } = useLoaderData();

  const navigate = useNavigate();








  return (

    <Form method="post" id="contact-form">
      {/* existing code */}




      <p>

        <button type="submit">Save</button>
        <button
          type="button"
          onClick={() => {
            navigate(-1);
          }}
        >
          Cancel
        </button>
      </p>
    </Form>
  );

}

新增一个菜单路由,在右侧点击 Cancel 按钮结果如下
图片.png
新增一个菜单路由
图片.png
取消后返回上一个路由图片.png

15.实现搜索功能

到目前为止,我们所有的交互功能都是更改 URL 的链接或将数据发送给操作的表单。

搜索功能也不列外,唯一不同的是搜索只改变 URL,它不改变数据。

现在搜索框只是一个普通的 HTML 的<form>,没有使用 React Router “< Form>”。

测试原生 HTML 的<form> 是怎么处理的,在搜索框中键入一个名称( Norush ),然后按回车键。注意此时浏览器的 URL 中包含输入的查询参数:

http://localhost:5173/?q=Norush

HTML 的<form>

<form id="search-form" role="search">
  <input

    id="q"

    aria-label="Search contacts"

    placeholder="Search"

    type="search"

    name="q"

  />

  <div id="search-spinner" aria-hidden hidden={true} />

  <div className="sr-only" aria-live="polite"></div>

</form>

结合段代码分析:
正如我们之前看到的,浏览器可以通过的 name 属性序列化表单。
这个输入的名称是 q,这就是为什么 URL 有?q=

如果我们将它name属性为 search, URL 将是?search=

http://localhost:5173/?search=Norush

**注意,**这个表单与我们使用过的其他表单不同,它没有<form method="post">。默认方法为“get”。这意味着当浏览器为下一个文档创建请求时,它不会将表单数据放入请求 POST 主体中,而是放入 GET 请求的URLSearchParams 中。

使用客户端路由获取提交
让我们使用客户端路由来提交这个表单,并在现有的加载器中过滤列表

? 将 HTLM  换成 React Router

<Form id="search-form" role="search">
  <input

    id="q"

    aria-label="Search contacts"

    placeholder="Search"

    type="search"

    name="q"

  />

  <div id="search-spinner" aria-hidden hidden={true} />

  <div className="sr-only" aria-live="polite"></div>

</Form>

? 如果有 URLSearchParams,则过滤列表

修改 src/routes/root.jsx 中的加载器 loader

export async function loader({ request }) {
  const url = new URL(request.url);
  const q = url.searchParams.get("q");
  const contacts = await getContacts(q);
  return { contacts };
}


输入A 回车调式结果如下
图片.png
因为使用的**

**默认这是一个 GET,而不是 POST,所以 React Router 不会调用这个动作。提交 GET 表单与单击表单与链接相同 ,都只是 URL 发生了变化。这就是为什么我们为过滤添加的代码是在加载器中,而不是在路由的动作中。
这也意味着它是一个普通的页面导航。你可以点击后退按钮回到原来的位置。

存在问题:
从调试的结果来看,目前的搜索功能还没有完善,仍然存在以下几个用户体验问题:

  • 如果在搜索后刷新页面,则表单字段中不再包含该值,即使列表已经过过滤
  • 如果在搜索后单击返回,表单字段仍然具有您输入的值,即使列表不再经过过滤。

换句话说,其实就是URL和表单状态不同步。哪怎么解决呢?
解决方式:
? 1.从加载器返回q,并将其设置为搜索字段的默认值

// existing code



















export async function loader({ request }) {
  const url = new URL(request.url);
  const q = url.searchParams.get("q");
  const contacts = await getContacts(q);
  return { contacts, q };
}

export default function Root() {
  const { contacts, q } = useLoaderData();
  const navigation = useNavigation();



  return (
    <>
      <div id="sidebar">
        <h1>React Router Contacts</h1>
        <div>
          <Form id="search-form" role="search">
            <input
              id="q"
              aria-label="Search contacts"
              placeholder="Search"
              type="search"
              name="q"
              defaultValue={q}
            />
            {/* existing code */}
          </Form>
          {/* existing code */}
        </div>
        {/* existing code */}
      </div>
      {/* existing code */}
    </>
  );
}

输入内容回车调式结果如下
图片.png这就解决了问题(1)。如果现在刷新页面,输入字段依然显示查询。

? **2.将输入值与URL搜索参数同步 **
相对于搜索功能来说,我们可能更希望过滤发生在每次击键时,而不是在表单显式提交时。而官方定义的 useSubmit 方法就能解决这个问题。

// existing code






import {
  // existing code
  useSubmit,
} from "react-router-dom";










export default function Root() {
  const { contacts, q } = useLoaderData();
  const navigation = useNavigation();
  const submit = useSubmit();


  return (

    <>

      <div id="sidebar">
        <h1>React Router Contacts</h1>
        <div>
          <Form id="search-form" role="search">
            <input
              id="q"
              aria-label="Search contacts"
              placeholder="Search"
              type="search"
              name="q"
              defaultValue={q}
              onChange={(event) => {
                submit(event.currentTarget.form);
              }}
            />

            {/* existing code */}
          </Form>
          {/* existing code */}
        </div>
        {/* existing code */}
      </div>
      {/* existing code */}
    </>

  );

}

现在,当你输入键值时,表单将自动提交!
注意要提交的参数。我们传入event.currentarget.form。currentTarget是事件附加的DOM节点,currentTarget.form是输入的父Form节点。submit函数将序列化并提交您传递给它的任何表单。

15.1.搜索迟钝问题

在生产应用程序中,这种搜索可能会在数据库中查找记录,而数据库太大,无法一次发送所有记录并过滤客户端。这就是为什么这个演示有一些伪造的网络延迟。
没有任何加载指示,搜索感觉有点迟钝。即使我们可以使我们的数据库更快,我们也总是会有用户的网络延迟,这是我们无法控制的。为了获得更好的用户体验,让我们为搜索添加一些即时UI反馈。为此,我们将再次使用 useNavigation。

// existing code



















export default function Root() {


  const { contacts, q } = useLoaderData();
  const navigation = useNavigation();
  const submit = useSubmit();



  const searching =
    navigation.location &&
    new URLSearchParams(navigation.location.search).has(
      "q"
    );



  useEffect(() => {
    document.getElementById("q").value = q;
  }, [q]);


  return (
    <>
      <div id="sidebar">
        <h1>React Router Contacts</h1>
        <div>
          <Form id="search-form" role="search">
            <input
              id="q"
              className={searching ? "loading" : ""}
              // existing code
            />

            <div
              id="search-spinner"
              aria-hidden
              hidden={!searching}
            />
            {/* existing code */}
          </Form>
          {/* existing code */}
        </div>
        {/* existing code */}
      </div>
      {/* existing code */}
    </>

  );

}

输入任意值调试结果如下
图片.png与全局响应挂载的方法一样,你会发现输入任意值时会有一个Pending 的效果。
navigation.location 会在应用导航到新URL并为其加载数据时显示。当没有待处理的导航时,它就会消失。

16.浏览器导航栏历史记录

现在每个按键都提交了表单,如果我们输入字符“seba”,然后用退格键删除它们,发现每个按键的内容都被添加到历史记录栈里边。
图片.png
为了避免每次输入,都会被添加到历史堆栈中。我们可以将历史堆栈中的当前条目替换为下一页,而不是将其推入其中。在这里可以使用 **useSubmit ** 的replace属性来解决问题

? 在提交中使用replace

 <Form id="search-form" role="search">
    <input
      id="q"
      // existing code
      onChange={(event) => {
        const isFirstSearch = q == null;
        submit(event.currentTarget.form, {
          replace: !isFirstSearch,
        });
      }}
    />
    {/* existing code */}
</Form>

我们只想替换搜索结果,而不是我们开始搜索之前的页面,所以会快速检查输入内容q 是否是第一次搜索( q == null ),然后决定替换。添加这个条件后,每个按键就不会创建新的条目,历史栈中也只会添加最终输入结果。调试结果如下
图片.png

到目前为止,我们所有改变数据的次数都使用了导航表单,在历史堆栈中创建新条目。虽然这些用户流很常见,但同样常见的方法还有,在不引用导航的情况下更改数据。

17.实现无导航更新

在HTML/HTTP中,数据变化和加载是用导航:*和’ 来建模的。在React Router中对应的是和。两者都会用到浏览器中的导航。但有时你想在不更改URL的情况下调用导航之外的加载器,或者调用一个操作(并获取页面上的数据以重新验证)。对于这些情况,官方提供了 **useFetcher **钩子。它能让我们在不引起导航的情况下与加载器和操作进行通信。

下面以收藏功能为例:
点联系人页面上的”☆” 收藏后。我们不需要创建或删除新记录,也不想更改页面,我们只想更改正在查看的页面上的数据
图片.png完成这个效果还需要以下几个步骤:
? 将表单更改为fetcher表单

import {



  useLoaderData,
  Form,
  useFetcher,
} from "react-router-dom";










// existing code





function Favorite({ contact }) {
  const fetcher = useFetcher();
  let favorite = contact.favorite;



  return (


    <fetcher.Form method="post">
      <button
        name="favorite"
        value={favorite ? "false" : "true"}
        aria-label={
          favorite
            ? "Remove from favorites"
            : "Add to favorites"
        }
      >
        {favorite ? "★" : "☆"}
      </button>
    </fetcher.Form>
  );

}

? 创建路由 action

// existing code






import { getContact, updateContact } from "../contacts";
















export async function action({ request, params }) {

  let formData = await request.formData();
  return updateContact(params.contactId, {
    favorite: formData.get("favorite") === "true",
  });
}





export default function Contact() {
  // existing code
}

? 在根路由配置路由的新动作

// existing code






import Contact, {
  loader as contactLoader,
  action as contactAction,
} from "./routes/contact";








const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    errorElement: <ErrorPage />,
    loader: rootLoader,
    action: rootAction,
    children: [
      { index: true, element: <Index /> },
      {

        path: "contacts/:contactId",

        element: <Contact />,

        loader: contactLoader,
        action: contactAction,
      },


      /* existing code */
    ],

  },

]); 

完成以上步骤后点击右侧用户的”☆” 收藏后,调式结果如下
图片.png
通过调式发现左侧菜单栏上的星星,与右侧的星星同步自动更新了。
新的 <fetcher.Form method=”post”>的工作原理与一直在使用的 几乎完全相同。
它们都是调用操作,然后自动重新验证所有数据,甚至您的错误也会以有同样的方式被捕获。
它们唯一的区别就是用 <fetcher.Form method=”post”> 方法导航时 URL不会改变,历史堆栈不受影响。

虽然到这里无导航更新数据的功能已经实现,但是你会发现点击”☆”还是有点迟钝。实际使用中网络延迟可能也会导致同样的效果。解决这个问题可以使用 fetcher 的 formData 属性获取表单的数据提前更新

? 从fetcher.formData中读 favorite 状态值

// existing code



















function Favorite({ contact }) {
  const fetcher = useFetcher();





  let favorite = contact.favorite;
  if (fetcher.formData) {
    favorite = fetcher.formData.get("favorite") === "true";
  }
  // existing code 
}

? 避免网络延迟,或错误情况,需要在加载器中抛出404响应

export async function loader({ params }) {
  const contact = await getContact(params.contactId);
  if (!contact) {
    throw new Response("", {
      status: 404,
      statusText: "Not Found",
    });
  }
  return { contact };
}

? 将子路由封装在无路径路由中
最后一件事。我们看到的最后一个错误页面最好是在根出口中呈现,而不是在整个页面中呈现。事实上,我们所有子路由中的每个错误都应该放在 中,这样用户就有了比点击刷新更多的选择。如果将error元素添加到每个子路由中,但由于它们可能都是相同的错误页面,因此不建议这样做。

有一个更干净的方法。根据冒泡就近原则,我们可以再套一层子路由,将errorElement属性统一放在根子路由上

createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    loader: rootLoader,
    action: rootAction,
    errorElement: <ErrorPage />,
    children: [

      {
        errorElement: <ErrorPage />,
        children: [
          { index: true, element: <Index /> },
          {
            path: "contacts/:contactId",
            element: <Contact />,
            loader: contactLoader,
            action: contactAction,
          },
          /* the rest of the routes */
        ],
      },


    ],

  },

]);

18.类一种配置风格|JSX Routes

许多人喜欢用JSX配置他们的路由。你可以用createRoutesFromElements来实现。在配置路由时,JSX和对象之间没有功能上的区别,这只是一种风格偏好。

import {



  createRoutesFromElements,
  createBrowserRouter,
  Route,
} from "react-router-dom";










const router = createBrowserRouter(
  createRoutesFromElements(
    <Route
      path="/"
      element={<Root />}
      loader={rootLoader}
      action={rootAction}
      errorElement={<ErrorPage />}
    >
      <Route errorElement={<ErrorPage />}>
        <Route index element={<Index />} />
        <Route
          path="contacts/:contactId"
          element={<Contact />}
          loader={contactLoader}
          action={contactAction}
        />

        <Route
          path="contacts/:contactId/edit"
          element={<EditContact />}
          loader={contactLoader}
          action={editAction}
        />
        <Route
          path="contacts/:contactId/destroy"
          action={destroyAction}
        />
      </Route>
    </Route>
  )
);

总结

通过这个案例可以学到了什么?
其一、理解Router6构建工程化时,createBrowserRouter 是怎么结合action,loadar,element,errorElement,path,childern实现交互配置的。
其二、同时也会更清楚userouteerror、useloaderdata、usenavigation、usenavigate、usesubmit、usefetcher、<outlet/><Link/><NavLink/><Form/> 等属性和方法的不同用途。

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MY5EdOgr' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片